From f73ba32653ceba39daee278a6e07a566c84bde3e Mon Sep 17 00:00:00 2001 From: huaiju Date: Fri, 5 Jun 2026 10:46:10 +0800 Subject: [PATCH 01/13] feat: add dt-skills cli support --- .gitignore | 17 + AGENTS.md | 158 + CLAUDE.md | 158 + app/controller/clawhub.js | 184 + app/model/skills_item.js | 11 + app/router.js | 18 + app/service/clawhub.js | 627 + app/service/skills.js | 635 +- app/utils/command-runner.js | 67 + app/utils/github-stars.js | 172 + app/utils/skill-install-key.js | 107 + app/view/app.js | 2 +- app/web/components/skills/SkillCard.scss | 251 + app/web/components/skills/SkillCard.tsx | 139 + .../skills/detail/SkillDetailContent.tsx | 731 +- .../detail/SkillSummaryModalContent.tsx | 4 +- app/web/pages/skills/detail/index.tsx | 4 +- app/web/pages/skills/detail/style.scss | 280 +- app/web/pages/skills/index.tsx | 147 +- app/web/pages/skills/style.scss | 95 +- app/web/pages/skills/types.ts | 4 +- app/web/router/index.ts | 4 + config/config.default.js | 13 +- contracts/skill-fingerprint/extensions.json | 48 + .../skill-fingerprint/golden-vectors.v1.json | 40 + contracts/skill-fingerprint/index.d.ts | 52 + contracts/skill-fingerprint/index.js | 149 + .../2026-06-08-fingerprint-file-contract.md | 42 + dt-skill/.gitignore | 5 + dt-skill/README.md | 164 + dt-skill/bin/dt-skill.js | 2 + dt-skill/package.json | 63 + dt-skill/pnpm-lock.yaml | 1091 + dt-skill/scripts/build.mjs | 19 + dt-skill/src/browserAuth.test.ts | 96 + dt-skill/src/browserAuth.ts | 174 + dt-skill/src/clawpack.ts | 145 + dt-skill/src/cli.test.ts | 38 + dt-skill/src/cli.ts | 710 + dt-skill/src/cli/agents.test.ts | 75 + dt-skill/src/cli/agents.ts | 51 + dt-skill/src/cli/authToken.ts | 13 + dt-skill/src/cli/buildInfo.ts | 95 + dt-skill/src/cli/clawdbotConfig.test.ts | 238 + dt-skill/src/cli/clawdbotConfig.ts | 204 + dt-skill/src/cli/commands/auth.test.ts | 56 + dt-skill/src/cli/commands/auth.ts | 145 + dt-skill/src/cli/commands/delete.test.ts | 136 + dt-skill/src/cli/commands/delete.ts | 162 + dt-skill/src/cli/commands/github.test.ts | 410 + dt-skill/src/cli/commands/github.ts | 417 + dt-skill/src/cli/commands/inspect.test.ts | 221 + dt-skill/src/cli/commands/inspect.ts | 445 + .../src/cli/commands/moderationPlan.test.ts | 41 + dt-skill/src/cli/commands/moderationPlan.ts | 64 + dt-skill/src/cli/commands/ownership.test.ts | 76 + dt-skill/src/cli/commands/ownership.ts | 113 + dt-skill/src/cli/commands/packages.test.ts | 2497 ++ dt-skill/src/cli/commands/packages.ts | 2075 ++ dt-skill/src/cli/commands/publish.test.ts | 483 + dt-skill/src/cli/commands/publish.ts | 315 + dt-skill/src/cli/commands/publishers.test.ts | 68 + dt-skill/src/cli/commands/publishers.ts | 47 + .../src/cli/commands/skills.install.test.ts | 362 + dt-skill/src/cli/commands/skills.test.ts | 926 + dt-skill/src/cli/commands/skills.ts | 1023 + dt-skill/src/cli/commands/star.ts | 37 + dt-skill/src/cli/commands/sync.test.ts | 555 + dt-skill/src/cli/commands/sync.ts | 214 + dt-skill/src/cli/commands/syncHelpers.test.ts | 26 + dt-skill/src/cli/commands/syncHelpers.ts | 405 + dt-skill/src/cli/commands/syncTypes.ts | 27 + dt-skill/src/cli/commands/transfer.test.ts | 132 + dt-skill/src/cli/commands/transfer.ts | 217 + dt-skill/src/cli/commands/unstar.ts | 39 + dt-skill/src/cli/helpStyle.ts | 45 + .../src/cli/prompts/search-multiselect.ts | 383 + dt-skill/src/cli/registry.test.ts | 93 + dt-skill/src/cli/registry.ts | 54 + dt-skill/src/cli/scanSkills.test.ts | 66 + dt-skill/src/cli/scanSkills.ts | 102 + dt-skill/src/cli/slug.ts | 16 + dt-skill/src/cli/types.ts | 15 + dt-skill/src/cli/ui.test.ts | 77 + dt-skill/src/cli/ui.ts | 137 + dt-skill/src/config.test.ts | 87 + dt-skill/src/config.ts | 83 + dt-skill/src/deviceAuth.test.ts | 151 + dt-skill/src/deviceAuth.ts | 151 + dt-skill/src/discovery.test.ts | 80 + dt-skill/src/discovery.ts | 23 + dt-skill/src/homedir.ts | 29 + dt-skill/src/http.bun.test.ts | 190 + dt-skill/src/http.test.ts | 412 + dt-skill/src/http.ts | 827 + dt-skill/src/schema/ark.ts | 29 + dt-skill/src/schema/clawScanNote.ts | 10 + dt-skill/src/schema/index.ts | 9 + dt-skill/src/schema/license.ts | 5 + dt-skill/src/schema/openclawContract.ts | 163 + dt-skill/src/schema/packages.ts | 751 + dt-skill/src/schema/routes.ts | 29 + dt-skill/src/schema/schemas.test.ts | 53 + dt-skill/src/schema/schemas.ts | 592 + .../schema/skillFingerprintContract.test.ts | 36 + .../src/schema/skillFingerprintContract.ts | 51 + dt-skill/src/schema/textFiles.test.ts | 31 + dt-skill/src/schema/textFiles.ts | 29 + dt-skill/src/skills.test.ts | 284 + dt-skill/src/skills.ts | 255 + dt-skill/test-artifact/cli.artifact.test.ts | 157 + dt-skill/test/cliCommandTestKit.ts | 101 + dt-skill/test/runtimeStubs.ts | 54 + dt-skill/tsconfig.json | 15 + dt-skill/vitest.artifact.config.ts | 12 + dt-skill/vitest.config.ts | 12 + package.json | 2 +- pnpm-lock.yaml | 24801 ++++++++++++++++ test/clawhub-contract.test.js | 874 + test/clawhub-integration.test.js | 177 + test/github-stars.test.js | 81 + test/skill-fingerprint-contract.test.js | 36 + test/skills-import-package-name.test.js | 134 + test/skills-install-key.test.js | 772 + test/skills-package-stars.test.js | 346 + 125 files changed, 51008 insertions(+), 990 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 app/controller/clawhub.js create mode 100644 app/service/clawhub.js create mode 100644 app/utils/command-runner.js create mode 100644 app/utils/github-stars.js create mode 100644 app/utils/skill-install-key.js create mode 100644 app/web/components/skills/SkillCard.scss create mode 100644 app/web/components/skills/SkillCard.tsx create mode 100644 contracts/skill-fingerprint/extensions.json create mode 100644 contracts/skill-fingerprint/golden-vectors.v1.json create mode 100644 contracts/skill-fingerprint/index.d.ts create mode 100644 contracts/skill-fingerprint/index.js create mode 100644 docs/superpowers/plans/2026-06-08-fingerprint-file-contract.md create mode 100644 dt-skill/.gitignore create mode 100644 dt-skill/README.md create mode 100755 dt-skill/bin/dt-skill.js create mode 100644 dt-skill/package.json create mode 100644 dt-skill/pnpm-lock.yaml create mode 100644 dt-skill/scripts/build.mjs create mode 100644 dt-skill/src/browserAuth.test.ts create mode 100644 dt-skill/src/browserAuth.ts create mode 100644 dt-skill/src/clawpack.ts create mode 100644 dt-skill/src/cli.test.ts create mode 100644 dt-skill/src/cli.ts create mode 100644 dt-skill/src/cli/agents.test.ts create mode 100644 dt-skill/src/cli/agents.ts create mode 100644 dt-skill/src/cli/authToken.ts create mode 100644 dt-skill/src/cli/buildInfo.ts create mode 100644 dt-skill/src/cli/clawdbotConfig.test.ts create mode 100644 dt-skill/src/cli/clawdbotConfig.ts create mode 100644 dt-skill/src/cli/commands/auth.test.ts create mode 100644 dt-skill/src/cli/commands/auth.ts create mode 100644 dt-skill/src/cli/commands/delete.test.ts create mode 100644 dt-skill/src/cli/commands/delete.ts create mode 100644 dt-skill/src/cli/commands/github.test.ts create mode 100644 dt-skill/src/cli/commands/github.ts create mode 100644 dt-skill/src/cli/commands/inspect.test.ts create mode 100644 dt-skill/src/cli/commands/inspect.ts create mode 100644 dt-skill/src/cli/commands/moderationPlan.test.ts create mode 100644 dt-skill/src/cli/commands/moderationPlan.ts create mode 100644 dt-skill/src/cli/commands/ownership.test.ts create mode 100644 dt-skill/src/cli/commands/ownership.ts create mode 100644 dt-skill/src/cli/commands/packages.test.ts create mode 100644 dt-skill/src/cli/commands/packages.ts create mode 100644 dt-skill/src/cli/commands/publish.test.ts create mode 100644 dt-skill/src/cli/commands/publish.ts create mode 100644 dt-skill/src/cli/commands/publishers.test.ts create mode 100644 dt-skill/src/cli/commands/publishers.ts create mode 100644 dt-skill/src/cli/commands/skills.install.test.ts create mode 100644 dt-skill/src/cli/commands/skills.test.ts create mode 100644 dt-skill/src/cli/commands/skills.ts create mode 100644 dt-skill/src/cli/commands/star.ts create mode 100644 dt-skill/src/cli/commands/sync.test.ts create mode 100644 dt-skill/src/cli/commands/sync.ts create mode 100644 dt-skill/src/cli/commands/syncHelpers.test.ts create mode 100644 dt-skill/src/cli/commands/syncHelpers.ts create mode 100644 dt-skill/src/cli/commands/syncTypes.ts create mode 100644 dt-skill/src/cli/commands/transfer.test.ts create mode 100644 dt-skill/src/cli/commands/transfer.ts create mode 100644 dt-skill/src/cli/commands/unstar.ts create mode 100644 dt-skill/src/cli/helpStyle.ts create mode 100644 dt-skill/src/cli/prompts/search-multiselect.ts create mode 100644 dt-skill/src/cli/registry.test.ts create mode 100644 dt-skill/src/cli/registry.ts create mode 100644 dt-skill/src/cli/scanSkills.test.ts create mode 100644 dt-skill/src/cli/scanSkills.ts create mode 100644 dt-skill/src/cli/slug.ts create mode 100644 dt-skill/src/cli/types.ts create mode 100644 dt-skill/src/cli/ui.test.ts create mode 100644 dt-skill/src/cli/ui.ts create mode 100644 dt-skill/src/config.test.ts create mode 100644 dt-skill/src/config.ts create mode 100644 dt-skill/src/deviceAuth.test.ts create mode 100644 dt-skill/src/deviceAuth.ts create mode 100644 dt-skill/src/discovery.test.ts create mode 100644 dt-skill/src/discovery.ts create mode 100644 dt-skill/src/homedir.ts create mode 100644 dt-skill/src/http.bun.test.ts create mode 100644 dt-skill/src/http.test.ts create mode 100644 dt-skill/src/http.ts create mode 100644 dt-skill/src/schema/ark.ts create mode 100644 dt-skill/src/schema/clawScanNote.ts create mode 100644 dt-skill/src/schema/index.ts create mode 100644 dt-skill/src/schema/license.ts create mode 100644 dt-skill/src/schema/openclawContract.ts create mode 100644 dt-skill/src/schema/packages.ts create mode 100644 dt-skill/src/schema/routes.ts create mode 100644 dt-skill/src/schema/schemas.test.ts create mode 100644 dt-skill/src/schema/schemas.ts create mode 100644 dt-skill/src/schema/skillFingerprintContract.test.ts create mode 100644 dt-skill/src/schema/skillFingerprintContract.ts create mode 100644 dt-skill/src/schema/textFiles.test.ts create mode 100644 dt-skill/src/schema/textFiles.ts create mode 100644 dt-skill/src/skills.test.ts create mode 100644 dt-skill/src/skills.ts create mode 100644 dt-skill/test-artifact/cli.artifact.test.ts create mode 100644 dt-skill/test/cliCommandTestKit.ts create mode 100644 dt-skill/test/runtimeStubs.ts create mode 100644 dt-skill/tsconfig.json create mode 100644 dt-skill/vitest.artifact.config.ts create mode 100644 dt-skill/vitest.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 test/clawhub-contract.test.js create mode 100644 test/clawhub-integration.test.js create mode 100644 test/github-stars.test.js create mode 100644 test/skill-fingerprint-contract.test.js create mode 100644 test/skills-import-package-name.test.js create mode 100644 test/skills-package-stars.test.js diff --git a/.gitignore b/.gitignore index 4f79674c..1762aee8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,20 @@ app/view # Research docs (working notes, not for commit) docs/research + +# Local Agent configuration & test skill folder +.agents/ +test-cli-skill/ + +# Universal/Dev tools ignores +dist/ +build/ +*.log +.env* +.DS_Store +Thumbs.db +*.tmp +*.swp +.vscode/ +.idea/ + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..cc0a54d9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,158 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +**项目**: dt-doraemon (哆啦A梦) — 开发者工具箱平台,包含代理服务、主机管理、配置中心、MCP 服务器注册中心、Skills 市场等模块。 + +**技术栈**: Egg.js 2.x + React 16 SSR + MySQL (Sequelize) + Webpack 4 + Redux + Socket.IO。 + +## 环境准备 + +项目使用 Node.js 18(`.nvmrc` 锁定为 `18.20.3`),依赖管理使用 Yarn。 + +```bash +# 切换到正确的 Node 版本 +fnm use 18 +# 或 +nvm use + +# 安装依赖 +yarn install +``` + +## 常用命令 + +```bash +# 开发启动(Egg.js dev + Webpack HMR,访问 http://127.0.0.1:7001) +npm run dev + +# 生产启动 +npm start + +# 构建前端资源 +npm run build + +# 代码检查 +npm run lint # prettier + eslint + stylelint +npm run lint:fix # 自动修复 +npm run check-types # TypeScript 类型检查(仅前端 app/web/) + +# 单独运行测试(主项目使用 Node.js 内置 test runner) +node --test test/*.test.js + +# dt-skill CLI 子项目(独立 npm 包) +cd dt-skill +npm run build +npm run test:src # vitest 测试 +node bin/dt-skill.js --help +``` + +## 项目结构 + +``` +app/ + controller/ # Egg.js 控制器(路由处理) + service/ # Egg.js 服务层(业务逻辑) + model/ # Sequelize 模型定义 + middleware/ # Egg.js 中间件(access, proxy) + schedule/ # Egg.js 定时任务 + web/ # React 前端源码(SSR) + pages/ # 页面组件 + router/ # React Router 配置 + store/ # Redux store(thunk + devtools) + api/ # API 请求封装(基于 url + method 映射) + layouts/ # 布局组件(basicLayout, sider, header) + mcp/ # MCP 服务器生命周期管理逻辑 + agent/ # Agent 进程 MCP 相关处理器 + utils/ # 后端工具函数 +config/ + config.default.js # Egg.js 默认配置 + config.local.js # 本地开发配置(含 MySQL、Webpack) + config.test.js # 测试环境配置 + config.prod.js # 生产配置 + plugin.js # Egg.js 插件注册(sequelize, reactssr, io, ssh) +dt-skill/ # ClawHub CLI 子项目(TypeScript + Vitest,独立包) +specs/ # 功能规格说明文档 +sql/ # 数据库初始化 SQL(doraemon.sql) +test/ # 主项目测试文件(Node.js 内置 test runner) +``` + +## 后端架构 + +### Egg.js 应用结构 +- **入口**: `app.js` (AppBootHook) 和 `agent.js` (Agent 进程生命周期)。 +- **路由**: `app/router.js` 集中定义所有 RESTful 路由和代理路由 `/proxy/:id/*`。 +- **MCP 生命周期**: Agent 进程 (`agent.js`) 负责 MCP 服务器的启动、停止、重启代理。Worker 进程通过 `app.messenger` 与 Agent 通信。 +- **定时任务**: `app/schedule/` 包含文章订阅和 MCP 健康检查任务。 +- **文件上传**: 使用 Egg.js multipart,临时文件存放于 `cache/uploads/`,大小限制 200MB,白名单允许所有类型。 + +### 数据库 +- **ORM**: Sequelize(`egg-sequelize` 插件)。 +- **配置位置**: `config/config.local.js`(本地默认数据库 `doraemon_test`,host `127.0.0.1`,用户名 `root`,密码 `123456`)。 +- **生产配置**: 从 `env.json` 的 `mysql.prod` 读取。 +- **初始化**: 导入 `sql/doraemon.sql` 到 MySQL。 + +### 关键配置 +- `env.json`: 存放 webhook URL、MySQL 配置、MCP 端口(`mcpEndpointPort: 7005`,`mcpInspectorWebPort: 7003`,`mcpInspectorServerPort: 7004`)。 +- CSRF 已全局禁用(`config.default.js`)。 +- `app.utils` 在 `app.js` 启动时挂载为全局工具对象。 + +## 前端架构 + +### 技术栈 +- React 16.9 + Redux + Redux-Thunk + React Router 4 +- Ant Design 4.15.6 + 自定义主题 (`theme.js`) +- TypeScript(仅用于前端 `app/web/`,编译目标 ES5) +- SCSS / Less(`app/web/scss/`,`app/web/pages/*/style.scss`) + +### 构建 +- Webpack 4 通过 `easywebpack-react` 配置(`webpack.config.js`)。 +- 开发时自动注入 CSS(`injectCss: true`)。 +- DLL 预打包:react, redux, react-router, xterm 等。 +- 路径别名:`@` -> `app/web/`,`@env` -> `env.json`。 +- 全局变量:`EASY_ENV_IS_DEV` 用于区分开发/生产环境。 + +### 状态管理 +- Redux store 在 `app/web/store/index.ts` 创建,使用 `redux-devtools-extension`(开发环境)。 +- Thunk 注入 `{ API }` 作为 extraArgument,API 通过 `app/web/api/index.ts` 根据 `url.ts` 中的 URL + method 配置自动生成。 + +### 路由 +- 前端路由定义在 `app/web/router/index.ts`,使用 `react-router-config` 风格配置。 +- 所有页面路由前缀为 `/page/*`,SSR 布局为 `BasicLayout`。 +- 部分页面使用 `react-loadable` 做代码分割(如 ConfigDetail, SwitchHostsEdit)。 + +## 子项目 dt-skill (ClawHub CLI) + +- **位置**: `dt-skill/`,独立的 npm 包,ES Module(`"type": "module"`)。 +- **用途**: 命令行工具,用于安装、搜索、发布 agent skills 和 OpenClaw 包。 +- **入口**: `bin/dt-skill.js`。 +- **构建**: `node ./scripts/build.mjs`,输出到 `dist/`。 +- **测试**: Vitest,配置在 `vitest.config.ts`(测试 `src/**/*.test.ts`)。 +- **Node 版本要求**: `>=20`(与主项目的 `>=18` 不同)。 + +## 测试 + +- **主项目**: 使用 Node.js 内置 `node:test` + `node:assert/strict`。测试文件在 `test/*.test.js`。 +- **dt-skill**: 使用 Vitest。运行 `cd dt-skill && npm run test:src`。 +- **原则**: 测试时不应该绕过待测组件用 curl 模拟。如果要测 CLI,运行真正的 CLI 命令;如果要测 API,通过客户端发请求。 + +## 代码规范 + +- ESLint 继承 `ko-lint-config`(`.eslintrc.js`)。 +- Prettier 继承 `ko-lint-config/.prettierrc`(`.prettierrc.js`)。 +- Stylelint 继承 `ko-lint-config/.stylelintrc`(`.stylelintrc.js`)。 +- Git 提交使用 Commitizen(`cz-conventional-changelog`),commit message 需符合 conventional commits 规范,由 husky + commitlint 校验。 + +## CI/CD + +- GitHub Actions: `.github/workflows/CI.yml`。 +- 在 `push` 到 `master` 或任意 `pull_request` 时触发。 +- 流程: 安装依赖 -> Prettier -> ESLint -> Stylelint -> check-types -> build。 +- Node 版本: 18.x,需设置 `NODE_OPTIONS=--openssl-legacy-provider`。 + +## 分支规范(CONTRIBUTING.md) + +- `master`: 主干分支,用于生产发布。 +- `dev`: 主开发分支。 +- `feat_版本号_xxx`: 新特性分支,从 `master` 切出,开发完 PR 到 `dev`。 +- `hotfix_版本号_xxx`: Bug 修复分支,从 `master` 切出,修复完 PR 到 `dev`,验证后合并到 `master`。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..89d48f2e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,158 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +**项目**: dt-doraemon (哆啦A梦) — 开发者工具箱平台,包含代理服务、主机管理、配置中心、MCP 服务器注册中心、Skills 市场等模块。 + +**技术栈**: Egg.js 2.x + React 16 SSR + MySQL (Sequelize) + Webpack 4 + Redux + Socket.IO。 + +## 环境准备 + +项目使用 Node.js 18(`.nvmrc` 锁定为 `18.20.3`),依赖管理使用 Yarn。 + +```bash +# 切换到正确的 Node 版本 +fnm use 18 +# 或 +nvm use + +# 安装依赖 +yarn install +``` + +## 常用命令 + +```bash +# 开发启动(Egg.js dev + Webpack HMR,访问 http://127.0.0.1:7001) +npm run dev + +# 生产启动 +npm start + +# 构建前端资源 +npm run build + +# 代码检查 +npm run lint # prettier + eslint + stylelint +npm run lint:fix # 自动修复 +npm run check-types # TypeScript 类型检查(仅前端 app/web/) + +# 单独运行测试(主项目使用 Node.js 内置 test runner) +node --test test/*.test.js + +# dt-skill CLI 子项目(独立 npm 包) +cd dt-skill +npm run build +npm run test:src # vitest 测试 +node bin/dt-skill.js --help +``` + +## 项目结构 + +``` +app/ + controller/ # Egg.js 控制器(路由处理) + service/ # Egg.js 服务层(业务逻辑) + model/ # Sequelize 模型定义 + middleware/ # Egg.js 中间件(access, proxy) + schedule/ # Egg.js 定时任务 + web/ # React 前端源码(SSR) + pages/ # 页面组件 + router/ # React Router 配置 + store/ # Redux store(thunk + devtools) + api/ # API 请求封装(基于 url + method 映射) + layouts/ # 布局组件(basicLayout, sider, header) + mcp/ # MCP 服务器生命周期管理逻辑 + agent/ # Agent 进程 MCP 相关处理器 + utils/ # 后端工具函数 +config/ + config.default.js # Egg.js 默认配置 + config.local.js # 本地开发配置(含 MySQL、Webpack) + config.test.js # 测试环境配置 + config.prod.js # 生产配置 + plugin.js # Egg.js 插件注册(sequelize, reactssr, io, ssh) +dt-skill/ # ClawHub CLI 子项目(TypeScript + Vitest,独立包) +specs/ # 功能规格说明文档 +sql/ # 数据库初始化 SQL(doraemon.sql) +test/ # 主项目测试文件(Node.js 内置 test runner) +``` + +## 后端架构 + +### Egg.js 应用结构 +- **入口**: `app.js` (AppBootHook) 和 `agent.js` (Agent 进程生命周期)。 +- **路由**: `app/router.js` 集中定义所有 RESTful 路由和代理路由 `/proxy/:id/*`。 +- **MCP 生命周期**: Agent 进程 (`agent.js`) 负责 MCP 服务器的启动、停止、重启代理。Worker 进程通过 `app.messenger` 与 Agent 通信。 +- **定时任务**: `app/schedule/` 包含文章订阅和 MCP 健康检查任务。 +- **文件上传**: 使用 Egg.js multipart,临时文件存放于 `cache/uploads/`,大小限制 200MB,白名单允许所有类型。 + +### 数据库 +- **ORM**: Sequelize(`egg-sequelize` 插件)。 +- **配置位置**: `config/config.local.js`(本地默认数据库 `doraemon_test`,host `127.0.0.1`,用户名 `root`,密码 `123456`)。 +- **生产配置**: 从 `env.json` 的 `mysql.prod` 读取。 +- **初始化**: 导入 `sql/doraemon.sql` 到 MySQL。 + +### 关键配置 +- `env.json`: 存放 webhook URL、MySQL 配置、MCP 端口(`mcpEndpointPort: 7005`,`mcpInspectorWebPort: 7003`,`mcpInspectorServerPort: 7004`)。 +- CSRF 已全局禁用(`config.default.js`)。 +- `app.utils` 在 `app.js` 启动时挂载为全局工具对象。 + +## 前端架构 + +### 技术栈 +- React 16.9 + Redux + Redux-Thunk + React Router 4 +- Ant Design 4.15.6 + 自定义主题 (`theme.js`) +- TypeScript(仅用于前端 `app/web/`,编译目标 ES5) +- SCSS / Less(`app/web/scss/`,`app/web/pages/*/style.scss`) + +### 构建 +- Webpack 4 通过 `easywebpack-react` 配置(`webpack.config.js`)。 +- 开发时自动注入 CSS(`injectCss: true`)。 +- DLL 预打包:react, redux, react-router, xterm 等。 +- 路径别名:`@` -> `app/web/`,`@env` -> `env.json`。 +- 全局变量:`EASY_ENV_IS_DEV` 用于区分开发/生产环境。 + +### 状态管理 +- Redux store 在 `app/web/store/index.ts` 创建,使用 `redux-devtools-extension`(开发环境)。 +- Thunk 注入 `{ API }` 作为 extraArgument,API 通过 `app/web/api/index.ts` 根据 `url.ts` 中的 URL + method 配置自动生成。 + +### 路由 +- 前端路由定义在 `app/web/router/index.ts`,使用 `react-router-config` 风格配置。 +- 所有页面路由前缀为 `/page/*`,SSR 布局为 `BasicLayout`。 +- 部分页面使用 `react-loadable` 做代码分割(如 ConfigDetail, SwitchHostsEdit)。 + +## 子项目 dt-skill (ClawHub CLI) + +- **位置**: `dt-skill/`,独立的 npm 包,ES Module(`"type": "module"`)。 +- **用途**: 命令行工具,用于安装、搜索、发布 agent skills 和 OpenClaw 包。 +- **入口**: `bin/dt-skill.js`。 +- **构建**: `node ./scripts/build.mjs`,输出到 `dist/`。 +- **测试**: Vitest,配置在 `vitest.config.ts`(测试 `src/**/*.test.ts`)。 +- **Node 版本要求**: `>=20`(与主项目的 `>=18` 不同)。 + +## 测试 + +- **主项目**: 使用 Node.js 内置 `node:test` + `node:assert/strict`。测试文件在 `test/*.test.js`。 +- **dt-skill**: 使用 Vitest。运行 `cd dt-skill && npm run test:src`。 +- **原则**: 测试时不应该绕过待测组件用 curl 模拟。如果要测 CLI,运行真正的 CLI 命令;如果要测 API,通过客户端发请求。 + +## 代码规范 + +- ESLint 继承 `ko-lint-config`(`.eslintrc.js`)。 +- Prettier 继承 `ko-lint-config/.prettierrc`(`.prettierrc.js`)。 +- Stylelint 继承 `ko-lint-config/.stylelintrc`(`.stylelintrc.js`)。 +- Git 提交使用 Commitizen(`cz-conventional-changelog`),commit message 需符合 conventional commits 规范,由 husky + commitlint 校验。 + +## CI/CD + +- GitHub Actions: `.github/workflows/CI.yml`。 +- 在 `push` 到 `master` 或任意 `pull_request` 时触发。 +- 流程: 安装依赖 -> Prettier -> ESLint -> Stylelint -> check-types -> build。 +- Node 版本: 18.x,需设置 `NODE_OPTIONS=--openssl-legacy-provider`。 + +## 分支规范(CONTRIBUTING.md) + +- `master`: 主干分支,用于生产发布。 +- `dev`: 主开发分支。 +- `feat_版本号_xxx`: 新特性分支,从 `master` 切出,开发完 PR 到 `dev`。 +- `hotfix_版本号_xxx`: Bug 修复分支,从 `master` 切出,修复完 PR 到 `dev`,验证后合并到 `master`。 diff --git a/app/controller/clawhub.js b/app/controller/clawhub.js new file mode 100644 index 00000000..25c4d6e5 --- /dev/null +++ b/app/controller/clawhub.js @@ -0,0 +1,184 @@ +const Controller = require('egg').Controller; + +class ClawhubController extends Controller { + // GET /.well-known/clawhub.json + async registryMetadata() { + const { ctx } = this; + const data = await ctx.service.clawhub.getRegistryMetadata(ctx.origin); + ctx.body = data; + } + + // GET /api/v1/search + async search() { + const { ctx } = this; + const { q, limit } = ctx.query; + const results = await ctx.service.clawhub.searchSkills(q, limit); + ctx.body = { results }; + } + + // GET /api/v1/skills + async list() { + const { ctx } = this; + const { cursor, sort, limit } = ctx.query; + const data = await ctx.service.clawhub.listSkills(cursor, sort, limit); + ctx.body = data; + } + + // GET /api/v1/skills/:slug + async detail() { + const { ctx } = this; + const { slug } = ctx.params; + const data = await ctx.service.clawhub.getSkillDetail(slug); + if (!data) { + ctx.status = 404; + ctx.body = { error: '技能不存在' }; + return; + } + ctx.body = data; + } + + // GET /api/v1/skills/:slug/versions + async versions() { + const { ctx } = this; + const { slug } = ctx.params; + const data = await ctx.service.clawhub.listSkillVersions(slug); + if (!data) { + ctx.status = 404; + ctx.body = { error: '技能不存在' }; + return; + } + ctx.body = data; + } + + // GET /api/v1/skills/:slug/versions/:version + async versionDetail() { + const { ctx } = this; + const { slug, version } = ctx.params; + const data = await ctx.service.clawhub.getSkillVersionDetail(slug, version); + if (!data) { + ctx.status = 404; + ctx.body = { error: '版本不存在' }; + return; + } + ctx.body = data; + } + + // GET /api/v1/skills/:slug/file + async fileContent() { + const { ctx } = this; + const { slug } = ctx.params; + const { path: filePath } = ctx.query; + const data = await ctx.service.clawhub.getSkillFileContent(slug, filePath); + if (!data) { + ctx.status = 404; + ctx.body = { error: '文件不存在' }; + return; + } + ctx.body = data.content; + } + + // GET /api/v1/download + async download() { + const { ctx } = this; + const { slug } = ctx.query; + const result = await ctx.service.clawhub.buildSkillZip(slug); + if (!result) { + ctx.status = 404; + ctx.body = { error: '技能不存在' }; + return; + } + ctx.set('Content-Type', 'application/zip'); + ctx.set( + 'Content-Disposition', + `attachment; filename="${encodeURIComponent(result.fileName)}"` + ); + ctx.body = result.content; + } + + // GET /api/v1/resolve + async resolve() { + const { ctx } = this; + const { slug, hash } = ctx.query; + const data = await ctx.service.clawhub.resolveFingerprint(slug, hash); + if (!data) { + ctx.status = 404; + ctx.body = { error: '未找到匹配的技能' }; + return; + } + ctx.body = data; + } + + // POST /api/v1/skills + async publish() { + const { ctx } = this; + const payload = ctx.request.body || {}; + const files = ctx.request.files + ? Array.isArray(ctx.request.files) + ? ctx.request.files + : [ctx.request.files] + : []; + + try { + // If multipart, payload comes as a JSON string field + let parsedPayload = payload; + if (payload.payload && typeof payload.payload === 'string') { + try { + parsedPayload = JSON.parse(payload.payload); + } catch (e) { + ctx.throw(400, 'payload 必须是有效的 JSON 字符串'); + } + } + + const result = await ctx.service.clawhub.publishSkill(parsedPayload, files); + ctx.body = result; + } finally { + try { + await ctx.cleanupRequestFiles(); + } catch (err) { + ctx.logger.warn('[clawhub] 清理临时上传文件失败:', err); + } + } + } + + // DELETE /api/v1/skills/:slug + async delete() { + const { ctx } = this; + const { slug } = ctx.params; + const result = await ctx.service.clawhub.deleteSkill(slug); + ctx.body = result; + } + + // POST /api/v1/skills/:slug/undelete + async undelete() { + const { ctx } = this; + const { slug } = ctx.params; + const result = await ctx.service.clawhub.undeleteSkill(slug); + ctx.body = result; + } + + // POST /api/v1/stars/:slug + async star() { + const { ctx } = this; + const { slug } = ctx.params; + const ip = ctx.service.skillLike.resolveClientIp(); + const data = await ctx.service.skillLike.like(slug, ip); + ctx.body = { + starred: data.liked, + starCount: data.likeCount, + }; + } + + // DELETE /api/v1/stars/:slug + async unstar() { + const { ctx } = this; + const { slug } = ctx.params; + const ip = ctx.service.skillLike.resolveClientIp(); + const data = await ctx.service.skillLike.unlike(slug, ip); + ctx.body = { + starred: data.liked, + starCount: data.likeCount, + }; + } +} + +module.exports = ClawhubController; diff --git a/app/model/skills_item.js b/app/model/skills_item.js index fcbd1b7f..7bbc531e 100644 --- a/app/model/skills_item.js +++ b/app/model/skills_item.js @@ -81,6 +81,17 @@ module.exports = (app) => { allowNull: false, defaultValue: 0, }, + is_package: { + type: TINYINT, + allowNull: false, + defaultValue: 0, + comment: '是否是技能包', + }, + parent_slug: { + type: STRING(255), + allowNull: true, + comment: '所属技能包的 slug', + }, created_at: { type: DATE, allowNull: false, diff --git a/app/router.js b/app/router.js index 2ce1e9b3..7877d5dd 100644 --- a/app/router.js +++ b/app/router.js @@ -162,6 +162,24 @@ module.exports = (app) => { app.post('/api/skills/unlike', app.controller.skillLike.unlike); app.get('/api/skills/like-status', app.controller.skillLike.getLikeStatus); + /** + * Clawhub Registry API (v1) + */ + app.get('/.well-known/clawhub.json', app.controller.clawhub.registryMetadata); + app.get('/api/v1/search', app.controller.clawhub.search); + app.get('/api/v1/skills', app.controller.clawhub.list); + app.get('/api/v1/skills/:slug', app.controller.clawhub.detail); + app.get('/api/v1/skills/:slug/versions', app.controller.clawhub.versions); + app.get('/api/v1/skills/:slug/versions/:version', app.controller.clawhub.versionDetail); + app.get('/api/v1/skills/:slug/file', app.controller.clawhub.fileContent); + app.get('/api/v1/download', app.controller.clawhub.download); + app.get('/api/v1/resolve', app.controller.clawhub.resolve); + app.post('/api/v1/skills', app.controller.clawhub.publish); + app.delete('/api/v1/skills/:slug', app.controller.clawhub.delete); + app.post('/api/v1/skills/:slug/undelete', app.controller.clawhub.undelete); + app.post('/api/v1/stars/:slug', app.controller.clawhub.star); + app.delete('/api/v1/stars/:slug', app.controller.clawhub.unstar); + // io.of('/').route('getShellCommand', io.controller.home.getShellCommand) // 暂时close Terminal相关功能 // io.of('/').route('loginServer', io.controller.home.loginServer) diff --git a/app/service/clawhub.js b/app/service/clawhub.js new file mode 100644 index 00000000..6ff47fa2 --- /dev/null +++ b/app/service/clawhub.js @@ -0,0 +1,627 @@ +const Service = require('egg').Service; +const AdmZip = require('adm-zip'); +const crypto = require('crypto'); +const fs = require('fs'); +const ignore = require('ignore'); +const path = require('path'); +const skillFingerprint = require('../../contracts/skill-fingerprint'); + +const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\w.-]+)?(?:\+[\w.-]+)?$/; +const SKILL_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +class ClawhubService extends Service { + async ensureStorageReady() { + if (this.ctx.service?.skills?.ensureStorageReady) { + await this.ctx.service.skills.ensureStorageReady(); + } + } + + // Well-Known Registry Metadata + async getRegistryMetadata(origin) { + return { + apiBase: origin, + authBase: null, + minCliVersion: '0.9.0', + }; + } + + // Search skills by name/description LIKE match + async searchSkills(query, limit = 20) { + await this.ensureStorageReady(); + const { SkillsItem } = this.app.model; + const { Op } = this.app.Sequelize; + const searchQuery = String(query || '').trim(); + const where = { is_delete: 0, parent_slug: null }; + + if (searchQuery) { + where[Op.or] = [ + { name: { [Op.like]: `%${searchQuery}%` } }, + { description: { [Op.like]: `%${searchQuery}%` } }, + ]; + } + + const skills = await SkillsItem.findAll({ + where, + limit: Math.min(Number(limit) || 20, 100), + order: [['stars', 'DESC']], + }); + + return skills.map((skill) => ({ + slug: skill.slug, + displayName: skill.name, + summary: skill.description || null, + version: skill.version || null, + score: 1.0, + updatedAt: skill.updated_at ? new Date(skill.updated_at).getTime() : null, + ownerHandle: null, + owner: null, + })); + } + + // List skills with cursor pagination and sorting + async listSkills(cursor, sort, limit = 20) { + await this.ensureStorageReady(); + const { SkillsItem } = this.app.model; + const where = { is_delete: 0, parent_slug: null }; + const sortMap = { + newest: { key: 'newest', field: 'updated_at', type: 'date' }, + createdAt: { key: 'newest', field: 'updated_at', type: 'date' }, + updated: { key: 'newest', field: 'updated_at', type: 'date' }, + downloads: { key: 'stars', field: 'stars', type: 'number' }, + stars: { key: 'stars', field: 'stars', type: 'number' }, + }; + const sortConfig = sortMap[sort] || sortMap.newest; + const order = [ + [sortConfig.field, 'DESC'], + ['id', 'DESC'], + ]; + const safeLimit = Math.min(Math.max(Number(limit) || 20, 1), 100); + + if (cursor) { + const decoded = this.decodeListCursor(cursor, sortConfig); + if (decoded) { + const { Op } = this.app.Sequelize; + where[Op.or] = [ + { [sortConfig.field]: { [Op.lt]: decoded.value } }, + { + [sortConfig.field]: decoded.value, + id: { [Op.lt]: decoded.id }, + }, + ]; + } + } + + const skills = await SkillsItem.findAll({ + where, + limit: safeLimit + 1, + order, + }); + + const hasMore = skills.length > safeLimit; + const items = hasMore ? skills.slice(0, -1) : skills; + const nextCursor = + hasMore && items.length > 0 + ? this.encodeListCursor(items[items.length - 1], sortConfig) + : null; + + return { + items: items.map((skill) => { + const tags = this.parseJsonArray(skill.tags); + const stats = { stars: skill.stars || 0, downloads: 0 }; + const item = { + slug: skill.slug, + displayName: skill.name, + summary: skill.description || null, + tags, + stats, + createdAt: skill.created_at ? new Date(skill.created_at).getTime() : null, + updatedAt: skill.updated_at ? new Date(skill.updated_at).getTime() : null, + }; + if (skill.version) { + item.latestVersion = { + version: skill.version, + createdAt: skill.updated_at ? new Date(skill.updated_at).getTime() : null, + changelog: '', + license: null, + }; + } + return item; + }), + nextCursor, + }; + } + + // Get skill detail + async _resolveSlug(slug) { + await this.ensureStorageReady(); + const { SkillsItem } = this.app.model; + let skill = await SkillsItem.findOne({ + where: { slug, is_delete: 0 }, + }); + + if (!skill && this.ctx.service?.skills?.ensureSkillCache) { + const skillCache = await this.ctx.service.skills.ensureSkillCache(); + const resolved = skillCache?.byInstallKey?.get(slug); + if (resolved) { + skill = await SkillsItem.findOne({ + where: { slug: resolved.slug, is_delete: 0 }, + }); + } + } + + return skill; + } + + async getSkillDetail(slug) { + const { SkillsItem } = this.app.model; + const skill = await this._resolveSlug(slug); + + if (!skill) { + return null; + } + + const version = skill.version || ''; + const tags = this.parseJsonArray(skill.tags); + const stats = { stars: skill.stars || 0, downloads: 0 }; + const createdAt = skill.created_at ? new Date(skill.created_at).getTime() : 0; + const updatedAt = skill.updated_at ? new Date(skill.updated_at).getTime() : 0; + + const detail = { + skill: { + slug: skill.slug, + displayName: skill.name, + summary: skill.description || null, + version, + tags, + stats, + createdAt, + updatedAt, + isPackage: skill.is_package === 1, + parentSlug: skill.parent_slug || null, + }, + latestVersion: version ? { + version, + createdAt: updatedAt, + changelog: '', + license: null, + } : null, + owner: null, + moderation: null, + }; + + if (skill.is_package === 1) { + const children = await SkillsItem.findAll({ + where: { parent_slug: skill.slug, is_delete: 0 }, + order: [['stars', 'DESC']], + }); + detail.skill.children = children.map((child) => ({ + slug: child.slug, + displayName: child.name, + summary: child.description || null, + version: child.version || null, + tags: this.parseJsonArray(child.tags), + stats: { stars: child.stars || 0, downloads: 0 }, + createdAt: child.created_at ? new Date(child.created_at).getTime() : 0, + updatedAt: child.updated_at ? new Date(child.updated_at).getTime() : 0, + isPackage: false, + parentSlug: child.parent_slug, + })); + } + + return detail; + } + + // List skill versions (single version only) + async listSkillVersions(slug) { + const skill = await this._resolveSlug(slug); + + if (!skill) { + return null; + } + + const version = skill.version || ''; + const createdAt = skill.updated_at ? new Date(skill.updated_at).getTime() : 0; + + return { + items: [ + { + version, + createdAt, + changelog: '', + changelogSource: null, + }, + ], + nextCursor: null, + }; + } + + // Get skill version detail + async getSkillVersionDetail(slug, version) { + const skill = await this._resolveSlug(slug); + + if (!skill) { + return null; + } + + const currentVersion = skill.version || ''; + if (currentVersion !== version) { + return null; + } + + const createdAt = skill.updated_at ? new Date(skill.updated_at).getTime() : 0; + + return { + version: { + version: currentVersion, + createdAt, + changelog: '', + changelogSource: null, + license: null, + }, + skill: { + slug: skill.slug, + displayName: skill.name, + }, + }; + } + + // Get skill file content + async getSkillFileContent(slug, filePath) { + const { SkillsFile } = this.app.model; + const skill = await this._resolveSlug(slug); + + if (!skill) { + return null; + } + + const targetPath = String(filePath || 'SKILL.md').trim(); + const file = await SkillsFile.findOne({ + where: { skill_id: skill.id, file_path: targetPath, is_delete: 0 }, + }); + + if (!file) { + return null; + } + + return { + content: file.content || '', + isBinary: file.is_binary === 1, + path: file.file_path, + }; + } + + // Build skill ZIP archive in memory + async buildSkillZip(slug) { + const { SkillsItem, SkillsFile } = this.app.model; + const skill = await this._resolveSlug(slug); + + if (!skill) { + return null; + } + + const zip = new AdmZip(); + + if (skill.is_package === 1) { + const children = await SkillsItem.findAll({ + where: { parent_slug: skill.slug, is_delete: 0 }, + }); + const sanitizeFileName = (fileName) => { + return String(fileName || 'skill') + .trim() + .replace(/[^a-zA-Z0-9._-]+/g, '-') + .replace(/^-+|-+$/g, '') + .toLowerCase(); + }; + for (const child of children) { + const childFolder = sanitizeFileName(child.name || child.slug || 'sub-skill'); + const childFiles = await SkillsFile.findAll({ + where: { skill_id: child.id, is_delete: 0 }, + }); + for (const file of childFiles) { + const content = file.content || ''; + const zipPath = path.posix.join(slug, childFolder, file.file_path); + if (file.is_binary === 1) { + zip.addFile(zipPath, Buffer.from(content, 'base64')); + } else { + zip.addFile(zipPath, Buffer.from(content, 'utf8')); + } + } + } + } else { + const files = await SkillsFile.findAll({ + where: { skill_id: skill.id, is_delete: 0 }, + }); + for (const file of files) { + const content = file.content || ''; + if (file.is_binary === 1) { + zip.addFile(file.file_path, Buffer.from(content, 'base64')); + } else { + zip.addFile(file.file_path, Buffer.from(content, 'utf8')); + } + } + } + + const version = skill.version || 'latest'; + return { + fileName: `${slug}-${version}.zip`, + content: zip.toBuffer(), + }; + } + + // Validate SemVer format + validateSemVer(version) { + return SEMVER_PATTERN.test(String(version || '').trim()); + } + + // Publish or update a skill + async publishSkill(payload, files) { + await this.ensureStorageReady(); + const { SkillsItem, SkillsFile, SkillsSource } = this.app.model; + const { slug, displayName, version, tags } = payload; + + if (!SKILL_SLUG_PATTERN.test(String(slug || ''))) { + this.ctx.throw(400, 'slug 格式无效'); + } + + if (!this.validateSemVer(version)) { + this.ctx.throw(400, 'version 必须是有效的 SemVer 格式'); + } + + const parsedTags = Array.isArray(tags) ? tags : []; + + // Read files content from disk and determine original filenames + const processedFiles = []; + for (const file of files) { + const originalName = file.filename || path.basename(file.filepath); + let content = ''; + let isBinary = false; + try { + if (file.filepath && fs.existsSync(file.filepath)) { + const buffer = fs.readFileSync(file.filepath); + isBinary = this.isBinaryBuffer(buffer); + if (isBinary) { + content = buffer.toString('base64'); + } else { + content = buffer.toString('utf8'); + } + } + } catch (err) { + this.ctx.logger.error(`[clawhub] 读取上传文件 ${originalName} 失败:`, err); + } + processedFiles.push({ + filename: originalName, + content, + isBinary, + }); + } + + // Check for SKILL.md + const skillMdFile = processedFiles.find( + (f) => f.filename && f.filename.toLowerCase().endsWith('skill.md') + ); + if (!skillMdFile) { + const uploadedNames = processedFiles.map((f) => f.filename).join(', '); + this.ctx.throw(400, `上传内容必须包含 SKILL.md。已上传: ${uploadedNames}`); + } + + // Upsert skill + let skill = await SkillsItem.findOne({ where: { slug } }); + + // Create or update source record + let source = await SkillsSource.findOne({ where: { source_url: 'clawhub-publish' } }); + if (!source) { + source = await SkillsSource.create({ + source_url: 'clawhub-publish', + source_type: 'upload', + clone_url: 'clawhub-publish', + source_repo: 'clawhub-publish', + }); + } + + if (skill) { + await skill.update({ + name: displayName, + description: payload.description || '', + version, + tags: JSON.stringify(parsedTags), + skill_md: skillMdFile.content || '', + is_delete: 0, + source_id: source.id, + }); + // Delete old files + await SkillsFile.update({ is_delete: 1 }, { where: { skill_id: skill.id } }); + } else { + skill = await SkillsItem.create({ + source_id: source.id, + slug, + name: displayName, + description: payload.description || '', + version, + tags: JSON.stringify(parsedTags), + skill_md: skillMdFile.content || '', + category: '通用', + file_count: files.length, + }); + } + + // Save files + for (const file of processedFiles) { + await SkillsFile.create({ + skill_id: skill.id, + file_path: file.filename, + language: this.detectLanguage(file.filename), + size: Buffer.byteLength(file.content, file.isBinary ? 'base64' : 'utf8'), + is_binary: file.isBinary ? 1 : 0, + encoding: file.isBinary ? 'base64' : 'utf8', + content: file.content, + }); + } + + // Update file count + await skill.update({ file_count: files.length }); + + return { + ok: true, + skillId: String(skill.id), + versionId: `v${version}`, + }; + } + + // Compute SHA256 fingerprint for a skill + async computeSkillFingerprint(skillId) { + await this.ensureStorageReady(); + const { SkillsFile } = this.app.model; + const files = await SkillsFile.findAll({ + where: { skill_id: skillId, is_delete: 0 }, + order: [['file_path', 'ASC']], + }); + const fingerprintIgnore = this.createFingerprintIgnore(files); + + return skillFingerprint.buildSkillFingerprintFromStoredFiles(files, { + ignoreMatcher: fingerprintIgnore, + }); + } + + // Resolve fingerprint to skill + async resolveFingerprint(slug, hash) { + await this.ensureStorageReady(); + const { SkillsItem } = this.app.model; + const skill = await SkillsItem.findOne({ + where: { slug, is_delete: 0 }, + }); + + if (!skill) { + return { + match: null, + latestVersion: null, + }; + } + + const skillFingerprint = await this.computeSkillFingerprint(skill.id); + const match = skillFingerprint === hash ? { version: skill.version || '' } : null; + const latestVersion = skill.version ? { version: skill.version } : null; + + return { + match, + latestVersion, + }; + } + + // Soft delete skill + async deleteSkill(slug) { + await this.ensureStorageReady(); + const { SkillsItem } = this.app.model; + const skill = await SkillsItem.findOne({ where: { slug } }); + if (!skill) { + this.ctx.throw(404, '技能不存在'); + } + await skill.update({ is_delete: 1 }); + return { ok: true }; + } + + // Undelete skill + async undeleteSkill(slug) { + await this.ensureStorageReady(); + const { SkillsItem } = this.app.model; + const skill = await SkillsItem.findOne({ where: { slug } }); + if (!skill) { + this.ctx.throw(404, '技能不存在'); + } + await skill.update({ is_delete: 0 }); + return { ok: true }; + } + + createFingerprintIgnore(files) { + const matcher = ignore(); + for (const ignoreFileName of skillFingerprint.FINGERPRINT_IGNORE_FILENAMES) { + const ignoreFile = files.find((file) => file.file_path === ignoreFileName); + if (ignoreFile && ignoreFile.is_binary !== 1 && ignoreFile.content) { + matcher.add(String(ignoreFile.content).split(/\r?\n/)); + } + } + return matcher; + } + + // Utility: parse JSON array string + parseJsonArray(value) { + if (!value) return []; + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + + encodeListCursor(skill, sortConfig) { + const rawValue = skill[sortConfig.field]; + const value = + sortConfig.type === 'date' ? new Date(rawValue).getTime() : Number(rawValue) || 0; + return Buffer.from( + JSON.stringify({ + sort: sortConfig.key, + value, + id: Number(skill.id), + }) + ).toString('base64'); + } + + decodeListCursor(cursor, sortConfig) { + try { + const parsed = JSON.parse(Buffer.from(cursor, 'base64').toString('utf8')); + if ( + parsed?.sort !== sortConfig.key || + !Number.isFinite(Number(parsed.value)) || + !Number.isFinite(Number(parsed.id)) + ) { + return null; + } + return { + value: + sortConfig.type === 'date' + ? new Date(Number(parsed.value)) + : Number(parsed.value), + id: Number(parsed.id), + }; + } catch { + return null; + } + } + + isBinaryBuffer(buffer) { + if (!buffer || buffer.length === 0) return false; + const sample = buffer.subarray(0, Math.min(buffer.length, 4096)); + if (sample.includes(0)) return true; + try { + new TextDecoder('utf-8', { fatal: true }).decode(sample); + return false; + } catch { + return true; + } + } + + // Utility: detect language from file extension + detectLanguage(fileName) { + const ext = (fileName || '').toLowerCase().slice(fileName.lastIndexOf('.') + 1); + const langMap = { + md: 'markdown', + js: 'javascript', + ts: 'typescript', + json: 'json', + yml: 'yaml', + yaml: 'yaml', + html: 'html', + css: 'css', + scss: 'scss', + less: 'less', + sh: 'bash', + py: 'python', + go: 'go', + rs: 'rust', + java: 'java', + }; + return langMap[ext] || 'text'; + } +} + +module.exports = ClawhubService; diff --git a/app/service/skills.js b/app/service/skills.js index ef76feeb..8ca4cd83 100644 --- a/app/service/skills.js +++ b/app/service/skills.js @@ -6,6 +6,13 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const fetch = require('node-fetch'); +const { + createInstallKeyMap, + resolveSkillIdentifier, + createUniqueSkillNames, +} = require('../utils/skill-install-key'); +const GitHubStarsClient = require('../utils/github-stars'); +const CommandRunner = require('../utils/command-runner'); const CACHE_TTL_MS = 60 * 1000; const MAX_FILE_LIST_COUNT = 2000; @@ -20,106 +27,6 @@ const SKILLS_ROOT_DISCOVER_DEPTH_LIMIT = 8; const DISCOVER_MAX_DIR_COUNT = 3000; const SKILL_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -function sanitizeInstallKeySegment(value) { - return String(value || '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .replace(/-{2,}/g, '-'); -} - -function createInstallKeyCandidates(skill = {}) { - const candidates = []; - const pushCandidate = (value) => { - const normalized = sanitizeInstallKeySegment(value); - if (normalized && !candidates.includes(normalized)) { - candidates.push(normalized); - } - }; - - pushCandidate(skill.name); - - const sourcePath = String(skill.sourcePath || '') - .trim() - .replace(/\\/g, '/'); - if (sourcePath) { - const segments = sourcePath.split('/').filter(Boolean); - if (segments.length > 0) { - pushCandidate(segments[segments.length - 1]); - } - } - - pushCandidate(skill.slug); - pushCandidate('skill'); - return candidates; -} - -function createInstallKeyMap(skills = []) { - const bySlug = new Map(); - const byInstallKey = new Map(); - const counts = new Map(); - const list = skills.map((skill) => { - const candidates = createInstallKeyCandidates(skill); - let installKey = candidates.find((candidate) => !byInstallKey.has(candidate)) || ''; - - if (!installKey) { - const baseKey = candidates[0] || 'skill'; - const nextCount = (counts.get(baseKey) || 1) + 1; - counts.set(baseKey, nextCount); - installKey = `${baseKey}-${nextCount}`; - while (byInstallKey.has(installKey)) { - const currentCount = (counts.get(baseKey) || nextCount) + 1; - counts.set(baseKey, currentCount); - installKey = `${baseKey}-${currentCount}`; - } - } else { - const baseKey = candidates[0] || installKey; - counts.set(baseKey, Math.max(counts.get(baseKey) || 1, 1)); - } - - const normalizedSkill = { - ...skill, - installKey, - }; - bySlug.set(normalizedSkill.slug, normalizedSkill); - byInstallKey.set(installKey, normalizedSkill); - return normalizedSkill; - }); - - return { - list, - bySlug, - byInstallKey, - }; -} - -function resolveSkillIdentifier(identifier, indexes = {}) { - const value = String(identifier || '').trim(); - if (!value) return null; - if (indexes.bySlug instanceof Map && indexes.bySlug.has(value)) { - return indexes.bySlug.get(value); - } - if (indexes.byInstallKey instanceof Map && indexes.byInstallKey.has(value)) { - return indexes.byInstallKey.get(value); - } - return null; -} - -function createUniqueSkillNames(skillNames = []) { - const values = []; - const seen = new Set(); - - skillNames.forEach((item) => { - const name = String(item || '').trim(); - if (!name) return; - if (seen.has(name)) return; - seen.add(name); - values.push(name); - }); - - return values; -} - const SKILL_CATEGORY_OPTIONS = [ '通用', '前端', @@ -173,6 +80,7 @@ class SkillsService extends Service { this.skillCache = null; this.storageReady = false; this.storageReadyPromise = null; + this.commandRunner = new CommandRunner({ defaultTimeout: GIT_COMMAND_TIMEOUT_MS }); } getSkillCategoryOptions() { @@ -217,6 +125,7 @@ class SkillsService extends Service { await SkillsItem.sync(); await SkillsFile.sync(); await this.ensureSkillsItemVersionColumn(); + await this.ensureSkillsItemPackageColumns(); this.storageReady = true; })(); @@ -240,6 +149,26 @@ class SkillsService extends Service { }); } + async ensureSkillsItemPackageColumns() { + const queryInterface = this.app.model.getQueryInterface(); + const table = await queryInterface.describeTable('skills_items'); + if (!table.is_package) { + await queryInterface.addColumn('skills_items', 'is_package', { + type: this.app.Sequelize.TINYINT, + allowNull: false, + defaultValue: 0, + comment: '是否是技能包', + }); + } + if (!table.parent_slug) { + await queryInterface.addColumn('skills_items', 'parent_slug', { + type: this.app.Sequelize.STRING(255), + allowNull: true, + comment: '所属技能包的 slug', + }); + } + } + parseJsonArray(value) { if (!value) return []; if (Array.isArray(value)) return value; @@ -268,6 +197,8 @@ class SkillsService extends Service { sourceRepo: skill.sourceRepo, sourcePath: skill.sourcePath, installCommand: skill.installCommand, + isPackage: skill.isPackage ? 1 : 0, + parentSlug: skill.parentSlug || null, }; } @@ -295,6 +226,8 @@ class SkillsService extends Service { skillMd: row.skill_md || '', installCommand: row.install_command || '', fileCount: Number(row.file_count) || 0, + isPackage: Number(row.is_package) || 0, + parentSlug: row.parent_slug || null, }; } @@ -340,7 +273,23 @@ class SkillsService extends Service { const safePageSize = Math.max(parseInt(pageSize, 10) || 20, 1); const { skills, categories } = this.skillCache; - let list = [...skills]; + // Aggregate child stars by parent slug for package star totals + const childStarsByParent = new Map(); + for (const item of skills) { + if (item.parentSlug) { + const current = childStarsByParent.get(item.parentSlug) || 0; + childStarsByParent.set(item.parentSlug, current + (Number(item.stars) || 0)); + } + } + + let list = [...skills] + .filter((item) => !item.parentSlug) + .map((item) => { + if (item.isPackage === 1 && childStarsByParent.has(item.slug)) { + return { ...item, stars: childStarsByParent.get(item.slug) }; + } + return item; + }); if (keyword) { const value = String(keyword).toLowerCase(); list = list.filter( @@ -405,7 +354,7 @@ class SkillsService extends Service { async getSkillDetail(slug) { await this.ensureSkillCache(); const skill = this.getSkillByIdentifier(slug); - const { SkillsFile } = this.app.model; + const { SkillsFile, SkillsItem } = this.app.model; const rows = await SkillsFile.findAll({ where: { @@ -417,11 +366,33 @@ class SkillsService extends Service { limit: MAX_FILE_LIST_COUNT, }); - return { + const detail = { ...this.toPublicSkill(skill), skillMd: skill.skillMd, fileList: rows.map((row) => row.file_path), }; + + if (skill.isPackage === 1) { + const children = await SkillsItem.findAll({ + where: { + parent_slug: skill.slug, + is_delete: 0, + }, + order: [ + ['stars', 'DESC'], + ['updated_at_remote', 'DESC'], + ['updated_at', 'DESC'], + ['id', 'DESC'], + ], + }); + detail.children = children.map((row) => this.toPublicSkill(this.toSkillDto(row))); + detail.stars = detail.children.reduce( + (sum, child) => sum + (Number(child.stars) || 0), + 0 + ); + } + + return detail; } async getSkillFileContent(slug, filePath) { @@ -461,24 +432,50 @@ class SkillsService extends Service { async getSkillArchive(slug) { await this.ensureSkillCache(); const skill = this.getSkillByIdentifier(slug); - const { SkillsFile } = this.app.model; - const rows = await SkillsFile.findAll({ - where: { - skill_id: skill.id, - is_delete: 0, - }, - order: [['file_path', 'ASC']], - limit: MAX_FILE_LIST_COUNT, - }); - + const { SkillsItem, SkillsFile } = this.app.model; const zip = new AdmZip(); const rootFolder = this.sanitizeFileName(skill.name || skill.slug || 'skill'); - rows.forEach((row) => { - const safeRelativePath = this.normalizeRelativePath(row.file_path); - const zipPath = path.posix.join(rootFolder, safeRelativePath); - const buffer = this.decodeStoredFileContent(row.content, Boolean(row.is_binary)); - zip.addFile(zipPath, buffer); - }); + + if (skill.isPackage === 1) { + const children = await SkillsItem.findAll({ + where: { + parent_slug: skill.slug, + is_delete: 0, + }, + }); + for (const child of children) { + const childFolder = this.sanitizeFileName(child.name || child.slug || 'sub-skill'); + const childFiles = await SkillsFile.findAll({ + where: { + skill_id: child.id, + is_delete: 0, + }, + order: [['file_path', 'ASC']], + limit: MAX_FILE_LIST_COUNT, + }); + childFiles.forEach((row) => { + const safeRelativePath = this.normalizeRelativePath(row.file_path); + const zipPath = path.posix.join(rootFolder, childFolder, safeRelativePath); + const buffer = this.decodeStoredFileContent(row.content, Boolean(row.is_binary)); + zip.addFile(zipPath, buffer); + }); + } + } else { + const rows = await SkillsFile.findAll({ + where: { + skill_id: skill.id, + is_delete: 0, + }, + order: [['file_path', 'ASC']], + limit: MAX_FILE_LIST_COUNT, + }); + rows.forEach((row) => { + const safeRelativePath = this.normalizeRelativePath(row.file_path); + const zipPath = path.posix.join(rootFolder, safeRelativePath); + const buffer = this.decodeStoredFileContent(row.content, Boolean(row.is_binary)); + zip.addFile(zipPath, buffer); + }); + } return { fileName: `${rootFolder}.zip`, @@ -600,11 +597,14 @@ class SkillsService extends Service { isLikelyBinary(buffer) { if (!buffer || buffer.length === 0) return false; - const sampleLength = Math.min(buffer.length, 1024); - for (let i = 0; i < sampleLength; i += 1) { - if (buffer[i] === 0) return true; + const sample = buffer.subarray(0, Math.min(buffer.length, 4096)); + if (sample.includes(0)) return true; + try { + new TextDecoder('utf-8', { fatal: true }).decode(sample); + return false; + } catch { + return true; } - return false; } sanitizeFileName(fileName) { @@ -633,11 +633,20 @@ class SkillsService extends Service { } buildSkillSlug(sourceMeta, relativeSkillPath, skillName, usedSlugs = new Set()) { - const sourceKey = `${sourceMeta.repoHost || 'local'}-${ - sourceMeta.repoPath || sourceMeta.sourceUrl || '' - }-${sourceMeta.ref || 'default'}`; - const relativeKey = String(relativeSkillPath || skillName || 'skill').replace(/\\/g, '/'); - const base = this.sanitizeSlugSegment(`${sourceKey}-${relativeKey}`) || 'skill'; + const isUpload = sourceMeta && (sourceMeta.repoHost === 'upload' || sourceMeta.sourceType === 'upload'); + const relativeKey = String( + (relativeSkillPath && relativeSkillPath !== '.') ? relativeSkillPath : (skillName || 'skill') + ).replace(/\\/g, '/'); + + let base; + if (isUpload) { + base = this.sanitizeSlugSegment(relativeKey) || 'skill'; + } else { + const sourceKey = `${sourceMeta.repoHost || 'local'}-${ + sourceMeta.repoPath || sourceMeta.sourceUrl || '' + }-${sourceMeta.ref || 'default'}`; + base = this.sanitizeSlugSegment(`${sourceKey}-${relativeKey}`) || 'skill'; + } let slug = base; if (slug.length > 220) { @@ -745,9 +754,14 @@ class SkillsService extends Service { const identityText = String(identityKey || '').trim(); const normalizedIdentity = this.sanitizeSlugSegment(identityText); const identityHash = identityText ? this.hashString(identityText).slice(0, 8) : ''; - const sourceKey = - [normalizedName, normalizedIdentity, identityHash].filter(Boolean).join('-') || - normalizedName; + + // repoPath 用于包名展示,保持简洁可读:有 identityKey 时直接用,否则 fallback 到文件名 + const repoPath = normalizedIdentity || normalizedName; + // sourceUrl 用于数据库 source 去重,可带 hash 确保唯一性 + const sourceKey = normalizedIdentity + ? `${normalizedIdentity}-${identityHash}` + : normalizedName; + return { sourceUrl: `upload://${sourceKey}.skill`, sourceType: 'upload', @@ -756,7 +770,7 @@ class SkillsService extends Service { ref: '', subpath: '', repoHost: 'upload', - repoPath: sourceKey, + repoPath, originalAction: 'upload', }; } @@ -764,11 +778,7 @@ class SkillsService extends Service { getUploadIdentityKey(skillRecords = [], preferredName = '') { const name = String(preferredName || '').trim(); if (name) return name; - return skillRecords - .map((item) => String(item?.name || '').trim()) - .filter(Boolean) - .sort((a, b) => a.localeCompare(b)) - .join('|'); + return ''; } extractDescription(content) { @@ -1091,14 +1101,14 @@ class SkillsService extends Service { sparseCloneArgs.push(parsedSource.cloneUrl, targetDir); try { - await this.runCommand( + await this.commandRunner.runCommand( 'git', sparseCloneArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env ); - await this.runCommand( + await this.commandRunner.runCommand( 'git', ['-C', targetDir, 'sparse-checkout', 'set', parsedSource.subpath], GIT_COMMAND_TIMEOUT_MS, @@ -1130,7 +1140,7 @@ class SkillsService extends Service { cloneArgs.push(parsedSource.cloneUrl, targetDir); try { - await this.runCommand('git', cloneArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env); + await this.commandRunner.runCommand('git', cloneArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env); } catch (error) { if (!parsedSource.ref) { throw error; @@ -1145,7 +1155,7 @@ class SkillsService extends Service { parsedSource.cloneUrl, targetDir, ]; - await this.runCommand('git', fallbackArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env); + await this.commandRunner.runCommand('git', fallbackArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env); } } @@ -1487,9 +1497,15 @@ class SkillsService extends Service { }; } + if (options.excludeSlugs && options.excludeSlugs.length > 0) { + where.slug = { + [Op.notIn]: options.excludeSlugs, + }; + } + const existing = await SkillsItem.findOne({ where, - attributes: ['id', 'name'], + attributes: ['id', 'name', 'slug'], transaction: options.transaction, }); @@ -1603,6 +1619,7 @@ class SkillsService extends Service { async importSkillFile(params = {}, file) { const skillName = String(params.skillName || '').trim(); + const packageName = String(params.packageName || '').trim(); const category = this.normalizeCategory(params.category); const tags = this.normalizePlatformTags(params.tags); const fileName = String((file && file.filename) || '').trim(); @@ -1638,11 +1655,14 @@ class SkillsService extends Service { this.ctx.throw(400, '.zip 包内未发现有效技能(缺少 SKILL.md)'); } + // skillName 仅用于覆盖单技能的名称;packageName 用于多技能时指定包名 if (skillName && discoveredSkillDirs.length !== 1) { this.ctx.throw(400, '填写技能名称时,.zip 包必须且只能包含一个技能目录'); } - const parsedSource = this.buildUploadSourceMeta(fileName, skillName); + // 多技能时优先使用 packageName 作为 identityKey,回退到 skillName + const identityKey = packageName || skillName; + const parsedSource = this.buildUploadSourceMeta(fileName, identityKey); const skillRecords = discoveredSkillDirs.map((skillDir) => { const record = this.prepareSkillRecord( @@ -1658,18 +1678,34 @@ class SkillsService extends Service { return record; }); - await this.assertSkillNamesUnique(skillRecords.map((item) => item.name)); + const tempUsedSlugs = new Set(); + const excludeSlugs = skillRecords.map((record) => + this.buildSkillSlug( + parsedSource, + record.sourcePath, + record.name, + tempUsedSlugs + ) + ); + + await this.assertSkillNamesUnique( + skillRecords.map((item) => item.name), + { + excludeSlugs, + } + ); const uploadSourceMeta = this.buildUploadSourceMeta( fileName, - this.getUploadIdentityKey(skillRecords, skillName) + this.getUploadIdentityKey(skillRecords, identityKey) ); sourceRecord = await this.upsertSourceRecord(uploadSourceMeta, 'syncing'); const importedSkills = await this.persistSkillsForSource( sourceRecord.id, uploadSourceMeta, - skillRecords + skillRecords, + packageName ); await sourceRecord.update({ @@ -1957,7 +1993,7 @@ class SkillsService extends Service { }; } - async persistSkillsForSource(sourceId, sourceMeta, skillRecords = []) { + async persistSkillsForSource(sourceId, sourceMeta, skillRecords = [], preferredPackageName = '') { const { SkillsItem, SkillsFile } = this.app.model; const { Op } = this.app.Sequelize; const repoStars = await this.fetchStarsBySourceRepo(sourceMeta.sourceRepo); @@ -1986,6 +2022,68 @@ class SkillsService extends Service { const usedSlugs = new Set(); const createdSkills = []; + let parentSlug = ''; + if (skillRecords.length > 1) { + let parentName = ''; + if (preferredPackageName) { + // CLI 批量上传时使用用户指定的包名 + parentName = preferredPackageName; + } else if (sourceMeta.repoHost === 'upload' || sourceMeta.sourceType === 'upload') { + parentName = sourceMeta.repoPath || 'uploaded-skills-package'; + } else { + const parts = (sourceMeta.repoPath || '').split('/'); + parentName = parts[parts.length - 1] || 'git-skills-package'; + } + parentSlug = this.buildSkillSlug( + sourceMeta, + '.', + parentName, + usedSlugs + ); + + const parentPayload = { + source_id: sourceId, + slug: parentSlug, + name: parentName, + description: `技能包,包含以下子技能:\n${skillRecords.map((r) => `- **${r.name}**: ${r.description || ''}`).join('\n')}`, + category: skillRecords[0].category || '通用', + version: '1.0.0', + tags: JSON.stringify(Array.from(new Set(skillRecords.flatMap((r) => r.tags || [])))), + allowed_tools: JSON.stringify(Array.from(new Set(skillRecords.flatMap((r) => r.allowedTools || [])))), + stars: resolvedStars, + updated_at_remote: new Date(), + source_repo: sourceMeta.sourceRepo || '', + source_path: '.', + skill_md: `# ${parentName}\n\n这是一个技能包,包含以下子技能:\n${skillRecords.map((r) => `- **${r.name}**: ${r.description || ''}`).join('\n')}`, + install_command: '', + file_count: skillRecords.reduce((acc, r) => acc + (r.files || []).length, 0), + is_delete: 0, + is_package: 1, + parent_slug: null, + }; + + const globalExistingParent = await SkillsItem.findOne({ + where: { slug: parentSlug }, + transaction, + }); + + let parentRow; + if (globalExistingParent) { + parentRow = globalExistingParent; + await globalExistingParent.update(parentPayload, { transaction }); + oldRowMap.delete(parentSlug); + } else { + parentRow = await SkillsItem.create(parentPayload, { transaction }); + } + + createdSkills.push({ + slug: parentSlug, + name: parentName, + sourceRepo: sourceMeta.sourceRepo || '', + sourcePath: '.', + }); + } + for (const record of skillRecords) { const slug = this.buildSkillSlug( sourceMeta, @@ -2010,12 +2108,23 @@ class SkillsService extends Service { install_command: record.installCommand, file_count: record.files.length, is_delete: 0, + is_package: 0, + parent_slug: parentSlug || null, }; - const existingRow = oldRowMap.get(slug); + const globalExisting = await SkillsItem.findOne({ + where: { slug }, + transaction, + }); + + if (globalExisting && globalExisting.is_delete === 0 && globalExisting.name !== record.name) { + this.ctx.throw(400, 'slug 已存在'); + } - let itemRow = existingRow; - if (existingRow) { - await existingRow.update(payload, { transaction }); + const targetRow = globalExisting || oldRowMap.get(slug); + let itemRow; + if (targetRow) { + itemRow = targetRow; + await targetRow.update(payload, { transaction }); oldRowMap.delete(slug); } else { itemRow = await SkillsItem.create(payload, { transaction }); @@ -2082,155 +2191,12 @@ class SkillsService extends Service { } async fetchStarsBySourceRepo(sourceRepo = '') { - const repoFullName = this.extractGitHubRepoFullName(sourceRepo); - if (!repoFullName) return null; - return await this.fetchGitHubRepoStars(repoFullName); - } - - extractGitHubRepoFullName(sourceRepo = '') { - const raw = String(sourceRepo || '').trim(); - if (!raw) return ''; - const normalized = raw.replace(/^git\+/, '').replace(/\.git$/, ''); - const sshMatch = normalized.match(/^git@github\.com:([^/]+)\/([^/]+)$/i); - if (sshMatch) { - return `${sshMatch[1]}/${sshMatch[2]}`; - } - const httpsMatch = normalized.match(/^https?:\/\/github\.com\/([^/]+)\/([^/#?]+)/i); - if (httpsMatch) { - return `${httpsMatch[1]}/${httpsMatch[2]}`; - } - return ''; - } - - parseCompactNumber(input) { - const raw = String(input || '') - .trim() - .replace(/,/g, '') - .toLowerCase(); - if (!raw) return null; - - const match = raw.match(/^(\d+(?:\.\d+)?)\s*([kmb])?$/i); - if (!match) return null; - - const value = Number(match[1]); - if (!Number.isFinite(value)) return null; - - const suffix = (match[2] || '').toLowerCase(); - if (!suffix) return Math.round(value); - if (suffix === 'k') return Math.round(value * 1000); - if (suffix === 'm') return Math.round(value * 1000 * 1000); - if (suffix === 'b') return Math.round(value * 1000 * 1000 * 1000); - return null; - } - - extractStarsFromGitHubHtml(html = '') { - const content = String(html || ''); - if (!content) return null; - - const titleMatch = content.match(/id="repo-stars-counter-star"[^>]*title="([^"]+)"/i); - if (titleMatch) { - const stars = this.parseCompactNumber(titleMatch[1]); - if (typeof stars === 'number' && Number.isFinite(stars) && stars >= 0) return stars; - } - - const ariaMatch = content.match(/id="repo-stars-counter-star"[^>]*aria-label="([^"]+)"/i); - if (ariaMatch) { - const numberLike = ariaMatch[1].match(/[\d,.]+\s*[kmb]?/i); - if (numberLike) { - const stars = this.parseCompactNumber(numberLike[0]); - if (typeof stars === 'number' && Number.isFinite(stars) && stars >= 0) return stars; - } - } - - const textMatch = content.match(/id="repo-stars-counter-star"[^>]*>([^<]+)= 0) return stars; - } - - return null; - } - - async fetchGitHubRepoStarsFromHtml(repoFullName) { - const url = `https://github.com/${repoFullName}`; - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), GITHUB_API_TIMEOUT_MS); - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'User-Agent': 'doraemon-skills-market', - Accept: 'text/html', - }, - signal: controller.signal, - }); - - if (!response.ok) { - this.ctx.logger.warn( - `[skills] HTML兜底获取 stars 失败: ${repoFullName}, status=${response.status}` - ); - return null; - } - - const html = await response.text(); - return this.extractStarsFromGitHubHtml(html); - } catch (error) { - this.ctx.logger.warn( - `[skills] HTML兜底获取 stars 异常: ${repoFullName}, ${error.message}` - ); - return null; - } finally { - clearTimeout(timer); - } - } - - async fetchGitHubRepoStars(repoFullName) { - if (!repoFullName) return null; - - const url = `https://api.github.com/repos/${repoFullName}`; - const headers = { - Accept: 'application/vnd.github+json', - 'User-Agent': 'doraemon-skills-market', - }; - - const token = this.resolveGitHubToken(); - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), GITHUB_API_TIMEOUT_MS); - - try { - const response = await fetch(url, { - method: 'GET', - headers, - signal: controller.signal, - }); - - if (!response.ok) { - this.ctx.logger.warn( - `[skills] 获取 GitHub stars 失败: ${repoFullName}, status=${response.status}` - ); - if (response.status === 403 || response.status === 429) { - return await this.fetchGitHubRepoStarsFromHtml(repoFullName); - } - return null; - } - - const data = await response.json(); - const stars = Number(data.stargazers_count); - if (!Number.isFinite(stars) || stars < 0) return null; - return stars; - } catch (error) { - this.ctx.logger.warn( - `[skills] 获取 GitHub stars 异常: ${repoFullName}, ${error.message}` - ); - return null; - } finally { - clearTimeout(timer); - } + const client = new GitHubStarsClient({ + token: this.resolveGitHubToken(), + timeoutMs: GITHUB_API_TIMEOUT_MS, + logger: this.ctx.logger, + }); + return client.fetchByRepoUrl(sourceRepo); } extractHostFromRemote(remoteUrl = '') { @@ -2351,70 +2317,11 @@ class SkillsService extends Service { return ['-c', `http.https://${host}/.extraHeader=Authorization: Basic ${basicToken}`]; } - runCommand( - command, - args = [], - timeout = GIT_COMMAND_TIMEOUT_MS, - cwd = process.cwd(), - env = process.env - ) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd, - env, - }); - - let stdout = ''; - let stderr = ''; - let timedOut = false; - - const timer = setTimeout(() => { - timedOut = true; - child.kill('SIGTERM'); - }, timeout); - - child.stdout.on('data', (chunk) => { - stdout += chunk.toString(); - }); - - child.stderr.on('data', (chunk) => { - stderr += chunk.toString(); - }); - - child.on('error', (error) => { - clearTimeout(timer); - reject(error); - }); - - child.on('close', (code) => { - clearTimeout(timer); - - if (timedOut) { - reject(new Error(`命令执行超时(${timeout}ms): ${command}`)); - return; - } - - if (code !== 0) { - const detail = this.trimCommandOutput(stderr || stdout); - reject(new Error(detail || `命令退出码: ${code}`)); - return; - } - - resolve({ stdout, stderr }); - }); - }); - } - - trimCommandOutput(content = '') { - const value = String(content || '').trim(); - if (!value) return ''; - const maxLength = 3000; - if (value.length <= maxLength) return value; - return value.slice(value.length - maxLength); - } } +const installKeyModule = require('../utils/skill-install-key'); + module.exports = SkillsService; -module.exports.createInstallKeyMap = createInstallKeyMap; -module.exports.resolveSkillIdentifier = resolveSkillIdentifier; -module.exports.createUniqueSkillNames = createUniqueSkillNames; +module.exports.createInstallKeyMap = installKeyModule.createInstallKeyMap; +module.exports.resolveSkillIdentifier = installKeyModule.resolveSkillIdentifier; +module.exports.createUniqueSkillNames = installKeyModule.createUniqueSkillNames; diff --git a/app/utils/command-runner.js b/app/utils/command-runner.js new file mode 100644 index 00000000..f40c8d24 --- /dev/null +++ b/app/utils/command-runner.js @@ -0,0 +1,67 @@ +const { spawn } = require('child_process'); + +const DEFAULT_TIMEOUT_MS = 120 * 1000; + +class CommandRunner { + constructor({ defaultTimeout = DEFAULT_TIMEOUT_MS } = {}) { + this.defaultTimeout = defaultTimeout; + } + + runCommand(command, args = [], timeout = this.defaultTimeout, cwd = process.cwd(), env = process.env) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd, + env, + }); + + let stdout = ''; + let stderr = ''; + let timedOut = false; + + const timer = setTimeout(() => { + timedOut = true; + child.kill('SIGTERM'); + }, timeout); + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('error', (error) => { + clearTimeout(timer); + reject(error); + }); + + child.on('close', (code) => { + clearTimeout(timer); + + if (timedOut) { + reject(new Error(`命令执行超时(${timeout}ms): ${command}`)); + return; + } + + if (code !== 0) { + const detail = this.trimCommandOutput(stderr || stdout); + reject(new Error(detail || `命令退出码: ${code}`)); + return; + } + + resolve({ stdout, stderr }); + }); + }); + } + + trimCommandOutput(content = '') { + const value = String(content || '').trim(); + if (!value) return ''; + const maxLength = 3000; + if (value.length <= maxLength) return value; + return value.slice(value.length - maxLength); + } +} + +module.exports = CommandRunner; diff --git a/app/utils/github-stars.js b/app/utils/github-stars.js new file mode 100644 index 00000000..f6ba65a4 --- /dev/null +++ b/app/utils/github-stars.js @@ -0,0 +1,172 @@ +const fetch = require('node-fetch'); + +const DEFAULT_TIMEOUT_MS = 10 * 1000; + +class GitHubStarsClient { + constructor({ token = '', timeoutMs = DEFAULT_TIMEOUT_MS, logger = null } = {}) { + this.token = token; + this.timeoutMs = timeoutMs; + this.logger = logger; + } + + async fetchByRepoUrl(sourceRepo = '') { + const repoFullName = this.extractGitHubRepoFullName(sourceRepo); + if (!repoFullName) return null; + return this.fetchGitHubRepoStars(repoFullName); + } + + extractGitHubRepoFullName(sourceRepo = '') { + const raw = String(sourceRepo || '').trim(); + if (!raw) return ''; + const normalized = raw.replace(/^git\+/, '').replace(/\.git$/, ''); + const sshMatch = normalized.match(/^git@github\.com:([^/]+)\/([^/]+)$/i); + if (sshMatch) { + return `${sshMatch[1]}/${sshMatch[2]}`; + } + const httpsMatch = normalized.match(/^https?:\/\/github\.com\/([^/]+)\/([^/#?]+)/i); + if (httpsMatch) { + return `${httpsMatch[1]}/${httpsMatch[2]}`; + } + return ''; + } + + parseCompactNumber(input) { + const raw = String(input || '') + .trim() + .replace(/,/g, '') + .toLowerCase(); + if (!raw) return null; + + const match = raw.match(/^(\d+(?:\.\d+)?)\s*([kmb])?$/i); + if (!match) return null; + + const value = Number(match[1]); + if (!Number.isFinite(value)) return null; + + const suffix = (match[2] || '').toLowerCase(); + if (!suffix) return Math.round(value); + if (suffix === 'k') return Math.round(value * 1000); + if (suffix === 'm') return Math.round(value * 1000 * 1000); + if (suffix === 'b') return Math.round(value * 1000 * 1000 * 1000); + return null; + } + + extractStarsFromGitHubHtml(html = '') { + const content = String(html || ''); + if (!content) return null; + + const titleMatch = content.match(/id="repo-stars-counter-star"[^>]*title="([^"]+)"/i); + if (titleMatch) { + const stars = this.parseCompactNumber(titleMatch[1]); + if (typeof stars === 'number' && Number.isFinite(stars) && stars >= 0) return stars; + } + + const ariaMatch = content.match(/id="repo-stars-counter-star"[^>]*aria-label="([^"]+)"/i); + if (ariaMatch) { + const numberLike = ariaMatch[1].match(/[\d,.]+\s*[kmb]?/i); + if (numberLike) { + const stars = this.parseCompactNumber(numberLike[0]); + if (typeof stars === 'number' && Number.isFinite(stars) && stars >= 0) return stars; + } + } + + const textMatch = content.match(/id="repo-stars-counter-star"[^>]*>([^<]+)= 0) return stars; + } + + return null; + } + + async fetchGitHubRepoStarsFromHtml(repoFullName) { + const url = `https://github.com/${repoFullName}`; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'doraemon-skills-market', + Accept: 'text/html', + }, + signal: controller.signal, + }); + + if (!response.ok) { + if (this.logger) { + this.logger.warn( + `[github-stars] HTML兜底获取 stars 失败: ${repoFullName}, status=${response.status}` + ); + } + return null; + } + + const html = await response.text(); + return this.extractStarsFromGitHubHtml(html); + } catch (error) { + if (this.logger) { + this.logger.warn( + `[github-stars] HTML兜底获取 stars 异常: ${repoFullName}, ${error.message}` + ); + } + return null; + } finally { + clearTimeout(timer); + } + } + + async fetchGitHubRepoStars(repoFullName) { + if (!repoFullName) return null; + + const url = `https://api.github.com/repos/${repoFullName}`; + const headers = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'doraemon-skills-market', + }; + + if (this.token) { + headers.Authorization = `Bearer ${this.token}`; + } + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const response = await fetch(url, { + method: 'GET', + headers, + signal: controller.signal, + }); + + if (!response.ok) { + if (this.logger) { + this.logger.warn( + `[github-stars] 获取 GitHub stars 失败: ${repoFullName}, status=${response.status}` + ); + } + if (response.status === 403 || response.status === 429) { + return await this.fetchGitHubRepoStarsFromHtml(repoFullName); + } + return null; + } + + const data = await response.json(); + const stars = Number(data.stargazers_count); + if (!Number.isFinite(stars) || stars < 0) return null; + return stars; + } catch (error) { + if (this.logger) { + this.logger.warn( + `[github-stars] 获取 GitHub stars 异常: ${repoFullName}, ${error.message}` + ); + } + return null; + } finally { + clearTimeout(timer); + } + } +} + +module.exports = GitHubStarsClient; diff --git a/app/utils/skill-install-key.js b/app/utils/skill-install-key.js new file mode 100644 index 00000000..465f852f --- /dev/null +++ b/app/utils/skill-install-key.js @@ -0,0 +1,107 @@ +function sanitizeInstallKeySegment(value) { + return String(value || '') + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .replace(/-{2,}/g, '-'); +} + +function createInstallKeyCandidates(skill = {}) { + const candidates = []; + const pushCandidate = (value) => { + const normalized = sanitizeInstallKeySegment(value); + if (normalized && !candidates.includes(normalized)) { + candidates.push(normalized); + } + }; + + pushCandidate(skill.name); + + const sourcePath = String(skill.sourcePath || '') + .trim() + .replace(/\\/g, '/'); + if (sourcePath) { + const segments = sourcePath.split('/').filter(Boolean); + if (segments.length > 0) { + pushCandidate(segments[segments.length - 1]); + } + } + + pushCandidate(skill.slug); + pushCandidate('skill'); + return candidates; +} + +function createInstallKeyMap(skills = []) { + const bySlug = new Map(); + const byInstallKey = new Map(); + const counts = new Map(); + const list = skills.map((skill) => { + const candidates = createInstallKeyCandidates(skill); + let installKey = candidates.find((candidate) => !byInstallKey.has(candidate)) || ''; + + if (!installKey) { + const baseKey = candidates[0] || 'skill'; + const nextCount = (counts.get(baseKey) || 1) + 1; + counts.set(baseKey, nextCount); + installKey = `${baseKey}-${nextCount}`; + while (byInstallKey.has(installKey)) { + const currentCount = (counts.get(baseKey) || nextCount) + 1; + counts.set(baseKey, currentCount); + installKey = `${baseKey}-${currentCount}`; + } + } else { + const baseKey = candidates[0] || installKey; + counts.set(baseKey, Math.max(counts.get(baseKey) || 1, 1)); + } + + const normalizedSkill = { + ...skill, + installKey, + }; + bySlug.set(normalizedSkill.slug, normalizedSkill); + byInstallKey.set(installKey, normalizedSkill); + return normalizedSkill; + }); + + return { + list, + bySlug, + byInstallKey, + }; +} + +function resolveSkillIdentifier(identifier, indexes = {}) { + const value = String(identifier || '').trim(); + if (!value) return null; + if (indexes.bySlug instanceof Map && indexes.bySlug.has(value)) { + return indexes.bySlug.get(value); + } + if (indexes.byInstallKey instanceof Map && indexes.byInstallKey.has(value)) { + return indexes.byInstallKey.get(value); + } + return null; +} + +function createUniqueSkillNames(skillNames = []) { + const values = []; + const seen = new Set(); + + skillNames.forEach((item) => { + const name = String(item || '').trim(); + if (!name) return; + if (seen.has(name)) return; + seen.add(name); + values.push(name); + }); + + return values; +} + +module.exports = { + sanitizeInstallKeySegment, + createInstallKeyCandidates, + createInstallKeyMap, + resolveSkillIdentifier, + createUniqueSkillNames, +}; diff --git a/app/view/app.js b/app/view/app.js index 2de73133..02ccac01 100644 --- a/app/view/app.js +++ b/app/view/app.js @@ -1 +1 @@ -!function(n,t){for(var e in t)n[e]=t[e]}(exports,function(n){var t={};function e(a){if(t[a])return t[a].exports;var r=t[a]={i:a,l:!1,exports:{}};return n[a].call(r.exports,r,r.exports,e),r.l=!0,r.exports}return e.m=n,e.c=t,e.d=function(n,t,a){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:a})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},e.t=function(n,t){if(1&t&&(n=e(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var a=Object.create(null);if(e.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var r in n)e.d(a,r,function(t){return n[t]}.bind(null,r));return a},e.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(t,"a",t),t},e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},e.p="/public/",e(e.s=74)}([function(n,t){n.exports=require("react")},function(n,t){n.exports=require("antd")},function(n,t,e){"use strict";n.exports=function(n){var t=[];return t.toString=function(){return this.map((function(t){var e=function(n,t){var e=n[1]||"",a=n[3];if(!a)return e;if(t&&"function"==typeof btoa){var r=(i=a,l=btoa(unescape(encodeURIComponent(JSON.stringify(i)))),s="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(l),"/*# ".concat(s," */")),o=a.sources.map((function(n){return"/*# sourceURL=".concat(a.sourceRoot||"").concat(n," */")}));return[e].concat(o).concat([r]).join("\n")}var i,l,s;return[e].join("\n")}(t,n);return t[2]?"@media ".concat(t[2]," {").concat(e,"}"):e})).join("")},t.i=function(n,e,a){"string"==typeof n&&(n=[[null,n,""]]);var r={};if(a)for(var o=0;o1&&void 0!==arguments[1]?arguments[1]:{},e=t.replace,o=void 0!==e&&e,c=t.prepend,d=void 0!==c&&c,p=[],b=0;b0&&r[r.length-1])||6!==o[0]&&2!==o[0])){i=0;continue}if(3===o[0]&&(!r||o[1]>r[0]&&o[1]0?a:e)(n)}},function(n,t,e){var a=e(51)("keys"),r=e(52);n.exports=function(n){return a[n]||(a[n]=r(n))}},function(n,t){n.exports=require("js-cookie")},function(n,t,e){"use strict";var a,r;Object.defineProperty(t,"__esModule",{value:!0}),t.SUBSCRIPTIONSENDTYPECN=t.SUBSCRIPTIONSENDTYPE=t.SUBSCRIPTIONSTATUS=void 0,function(n){n[n.OPEN=1]="OPEN",n[n.CLOSE=2]="CLOSE"}(t.SUBSCRIPTIONSTATUS||(t.SUBSCRIPTIONSTATUS={})),function(n){n[n.FRIDAY=0]="FRIDAY",n[n.WORKDAY=1]="WORKDAY",n[n.EVERYDAY=2]="EVERYDAY",n[n.MONDAY=3]="MONDAY"}(r=t.SUBSCRIPTIONSENDTYPE||(t.SUBSCRIPTIONSENDTYPE={})),t.SUBSCRIPTIONSENDTYPECN=((a={})[r.MONDAY]="每周一",a[r.FRIDAY]="每周五",a[r.WORKDAY]="周一至周五",a[r.EVERYDAY]="每天",a)},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.urlReg=void 0,t.urlReg=new RegExp(/(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/,"i")},function(n,t){n.exports=require("socket.io-parser")},function(n,t){n.exports=require("prop-types")},function(n,t,e){"use strict";var a=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var r=a(e(0)),o=e(1),i=e(4);t.default=function(n){var t=n.status,e=n.errorMsg;return r.default.createElement("div",{style:{display:"inline-flex",alignItems:"center",gap:4}},function(){switch(t){case"running":return r.default.createElement(o.Badge,{status:"success",text:"运行中"});case"stopped":return r.default.createElement(o.Badge,{status:"default",text:"已停止"});case"error":return r.default.createElement("span",{style:{display:"inline-flex",alignItems:"center",gap:4}},r.default.createElement(o.Badge,{status:"error",text:"运行错误"}),r.default.createElement(o.Tooltip,{title:e},r.default.createElement(i.InfoCircleOutlined,{style:{color:"#999"}})));default:return r.default.createElement(o.Badge,{status:"warning",text:"未知"})}}())}},function(n,t,e){"use strict";var a=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var r=a(e(0)),o=e(1),i=e(4);e(235);t.default=function(n){var t=function(n){switch(n){case"stdio":return{color:"blue",icon:r.default.createElement(i.ApiOutlined,null),name:"STDIO",description:"原始传输方式:标准输入输出"};case"streamable-http":return{color:"green",icon:r.default.createElement(i.CloudServerOutlined,null),name:"HTTP",description:"原始传输方式:HTTP传输"};case"sse":return{color:"orange",icon:r.default.createElement(i.ThunderboltOutlined,null),name:"SSE",description:"原始传输方式:SSE流式传输"};default:return{color:"default",icon:r.default.createElement(i.ExclamationCircleOutlined,null),name:"UNKNOWN",description:"未知传输方式"}}}(n.transport);return r.default.createElement(o.Tooltip,{title:t.description,placement:"top"},r.default.createElement(o.Tag,{color:t.color,className:"transport-tag",icon:t.icon},t.name))}},function(n,t,e){"use strict";var a=e(16);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=a(e(81)),o=a(e(82)),i=a(e(39)),l={lang:(0,r.default)({placeholder:"请选择日期",yearPlaceholder:"请选择年份",quarterPlaceholder:"请选择季度",monthPlaceholder:"请选择月份",weekPlaceholder:"请选择周",rangePlaceholder:["开始日期","结束日期"],rangeYearPlaceholder:["开始年份","结束年份"],rangeMonthPlaceholder:["开始月份","结束月份"],rangeWeekPlaceholder:["开始周","结束周"]},o.default),timePickerLocale:(0,r.default)({},i.default)};l.lang.ok="确 定";var s=l;t.default=s},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a={placeholder:"请选择时间",rangePlaceholder:["开始时间","结束时间"]};t.default=a},function(n,t,e){"use strict";var a=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var r=a(e(84)),o=a(e(123)),i=a(e(133)),l=a(e(139)),s=a(e(147)),c=a(e(151)),d=a(e(157)),p=a(e(161)),b=a(e(188)),u=a(e(192)),f=a(e(201)),m=a(e(209)),h=a(e(210)),g=a(e(212)),x=a(e(220)),w=a(e(233)),k=a(e(241)),v=a(e(251)),y=a(e(260)),z=a(e(263)),_=[{path:"/",component:r.default,routes:[{path:"".concat("/page","/toolbox"),component:x.default},{path:"".concat("/page","/home"),component:d.default},{path:"".concat("/page","/internal-url-navigation"),component:b.default},{path:"".concat("/page","/proxy-server"),component:f.default},{path:"".concat("/page","/mail-sign"),component:u.default},{path:"".concat("/page","/host-management"),component:p.default},{path:"".concat("/page","/env-management"),component:s.default},{path:"".concat("/page","/config-center"),component:i.default},{path:"".concat("/page","/config-detail/:id"),component:l.default},{path:"".concat("/page","/switch-hosts-list"),component:m.default},{path:"".concat("/page","/switch-hosts-edit/:id/:type"),component:h.default},{path:"".concat("/page","/article-subscription-list"),component:o.default},{path:"".concat("/page","/tags"),component:g.default},{path:"".concat("/page","/mcp-server-market"),component:w.default},{path:"".concat("/page","/mcp-server-registry/edit/:serverId"),component:k.default},{path:"".concat("/page","/mcp-server-registry"),component:k.default},{path:"".concat("/page","/mcp-server-detail/:serverId"),component:v.default},{path:"".concat("/page","/mcp-server-inspector"),component:y.default},{path:"".concat("/page","/mcp-server-management"),component:z.default},{path:"*",component:c.default}]}];t.default=_},function(n,t){n.exports=require("classnames")},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.changeLocalIp=void 0;var a=e(1),r=e(43);t.changeLocalIp=function(n){return void 0===n&&(n=!1),function(t,e,o){o.API.getLocalIp().then((function(e){var o=e.success,i=e.data;o&&(console.log(i),n&&a.message.success("刷新成功!"),t({type:r.CHANGE_LOCAL_IP,payload:i}))})).catch((function(){n&&a.message.warning("刷新失败!")}))}}},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CHANGE_LOCAL_IP=void 0,t.CHANGE_LOCAL_IP="CHANGE_LOCAL_IP"},function(n,t,e){e(94);for(var a=e(12),r=e(15),o=e(14),i=e(9)("toStringTag"),l="CSSRuleList,CSSStyleDeclaration,CSSValueList,ClientRectList,DOMRectList,DOMStringList,DOMTokenList,DataTransferItemList,FileList,HTMLAllCollection,HTMLCollection,HTMLFormElement,HTMLSelectElement,MediaList,MimeTypeArray,NamedNodeMap,NodeList,PaintRequestList,Plugin,PluginArray,SVGLengthList,SVGNumberList,SVGPathSegList,SVGPointList,SVGStringList,SVGTransformList,SourceBufferList,StyleSheetList,TextTrackCueList,TextTrackList,TouchList".split(","),s=0;s=t.length?{value:void 0,done:!0}:(n=a(t,e),this._i+=n.length,{value:n,done:!1})}))},function(n,t,e){var a=e(45),r=e(9)("toStringTag"),o="Arguments"==a(function(){return arguments}());n.exports=function(n){var t,e,i;return void 0===n?"Undefined":null===n?"Null":"string"==typeof(e=function(n,t){try{return n[t]}catch(n){}}(t=Object(n),r))?e:o?a(t):"Object"==(i=a(t))&&"function"==typeof t.callee?"Arguments":i}},function(n,t,e){n.exports=e.p+"img/help-icon.3d0854a5.png"},function(n,t){n.exports=require("react-codemirror2")},function(n,t,e){"use strict";var a=this&&this.__assign||function(){return(a=Object.assign||function(n){for(var t,e=1,a=arguments.length;e*\/]/.test(p)?l(null,"select-op"):/[;{}:\[\]]/.test(p)?l(null,p):(n.eatWhile(/[\w\\\-]/),l("variable","variable")):l(null,"compare"):void l(null,"compare")}function c(n,t){for(var e,a=!1;null!=(e=n.next());){if(a&&"/"==e){t.tokenize=s;break}a="*"==e}return l("comment","comment")}function d(n,t){for(var e,a=0;null!=(e=n.next());){if(a>=2&&">"==e){t.tokenize=s;break}a="-"==e?a+1:0}return l("comment","comment")}return{startState:function(n){return{tokenize:s,baseIndent:n||0,stack:[]}},token:function(n,t){if(n.eatSpace())return null;e=null;var a=t.tokenize(n,t),r=t.stack[t.stack.length-1];return"hash"==e&&"rule"==r?a="atom":"variable"==a&&("rule"==r?a="number":r&&"@media{"!=r||(a="tag")),"rule"==r&&/^[\{\};]$/.test(e)&&t.stack.pop(),"{"==e?"@media"==r?t.stack[t.stack.length-1]="@media{":t.stack.push("{"):"}"==e?t.stack.pop():"@media"==e?t.stack.push("@media"):"{"==r&&"comment"!=e&&t.stack.push("rule"),a},indent:function(n,t){var e=n.stack.length;return/^\}/.test(t)&&(e-="rule"==n.stack[n.stack.length-1]?2:1),n.baseIndent+e*i},electricChars:"}"}})),n.defineMIME("text/x-nginx-conf","nginx")}(e(61))},function(n,t,e){n.exports=function(){"use strict";var n=navigator.userAgent,t=navigator.platform,e=/gecko\/\d/i.test(n),a=/MSIE \d/.test(n),r=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(n),o=/Edge\/(\d+)/.exec(n),i=a||r||o,l=i&&(a?document.documentMode||6:+(o||r)[1]),s=!o&&/WebKit\//.test(n),c=s&&/Qt\/\d+\.\d+/.test(n),d=!o&&/Chrome\/(\d+)/.exec(n),p=d&&+d[1],b=/Opera\//.test(n),u=/Apple Computer/.test(navigator.vendor),f=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(n),m=/PhantomJS/.test(n),h=u&&(/Mobile\/\w+/.test(n)||navigator.maxTouchPoints>2),g=/Android/.test(n),x=h||g||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(n),w=h||/Mac/.test(t),k=/\bCrOS\b/.test(n),v=/win/i.test(t),y=b&&n.match(/Version\/(\d*\.\d*)/);y&&(y=Number(y[1])),y&&y>=15&&(b=!1,s=!0);var z=w&&(c||b&&(null==y||y<12.11)),_=e||i&&l>=9;function F(n){return new RegExp("(^|\\s)"+n+"(?:$|\\s)\\s*")}var E,C=function(n,t){var e=n.className,a=F(t).exec(e);if(a){var r=e.slice(a.index+a[0].length);n.className=e.slice(0,a.index)+(r?a[1]+r:"")}};function S(n){for(var t=n.childNodes.length;t>0;--t)n.removeChild(n.firstChild);return n}function O(n,t){return S(n).appendChild(t)}function D(n,t,e,a){var r=document.createElement(n);if(e&&(r.className=e),a&&(r.style.cssText=a),"string"==typeof t)r.appendChild(document.createTextNode(t));else if(t)for(var o=0;o=t)return i+(t-o);i+=l-o,i+=e-i%e,o=l+1}}h?B=function(n){n.selectionStart=0,n.selectionEnd=n.value.length}:i&&(B=function(n){try{n.select()}catch(n){}});var U=function(){this.id=null,this.f=null,this.time=0,this.handler=N(this.onTimeout,this)};function H(n,t){for(var e=0;e=t)return a+Math.min(i,t-r);if(r+=o-a,a=o+1,(r+=e-r%e)>=t)return a}}var V=[""];function G(n){for(;V.length<=n;)V.push($(V)+" ");return V[n]}function $(n){return n[n.length-1]}function J(n,t){for(var e=[],a=0;a"€"&&(n.toUpperCase()!=n.toLowerCase()||tn.test(n))}function an(n,t){return t?!!(t.source.indexOf("\\w")>-1&&en(n))||t.test(n):en(n)}function rn(n){for(var t in n)if(n.hasOwnProperty(t)&&n[t])return!1;return!0}var on=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function ln(n){return n.charCodeAt(0)>=768&&on.test(n)}function sn(n,t,e){for(;(e<0?t>0:te?-1:1;;){if(t==e)return t;var r=(t+e)/2,o=a<0?Math.ceil(r):Math.floor(r);if(o==t)return n(o)?t:e;n(o)?e=o:t=o+a}}var dn=null;function pn(n,t,e){var a;dn=null;for(var r=0;rt)return r;o.to==t&&(o.from!=o.to&&"before"==e?a=r:dn=r),o.from==t&&(o.from!=o.to&&"before"!=e?a=r:dn=r)}return null!=a?a:dn}var bn=function(){var n=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,t=/[stwN]/,e=/[LRr]/,a=/[Lb1n]/,r=/[1n]/;function o(n,t,e){this.level=n,this.from=t,this.to=e}return function(i,l){var s="ltr"==l?"L":"R";if(0==i.length||"ltr"==l&&!n.test(i))return!1;for(var c,d=i.length,p=[],b=0;b-1&&(a[t]=r.slice(0,o).concat(r.slice(o+1)))}}}function xn(n,t){var e=hn(n,t);if(e.length)for(var a=Array.prototype.slice.call(arguments,2),r=0;r0}function yn(n){n.prototype.on=function(n,t){mn(this,n,t)},n.prototype.off=function(n,t){gn(this,n,t)}}function zn(n){n.preventDefault?n.preventDefault():n.returnValue=!1}function _n(n){n.stopPropagation?n.stopPropagation():n.cancelBubble=!0}function Fn(n){return null!=n.defaultPrevented?n.defaultPrevented:0==n.returnValue}function En(n){zn(n),_n(n)}function Cn(n){return n.target||n.srcElement}function Sn(n){var t=n.which;return null==t&&(1&n.button?t=1:2&n.button?t=3:4&n.button&&(t=2)),w&&n.ctrlKey&&1==t&&(t=3),t}var On,Dn,In=function(){if(i&&l<9)return!1;var n=D("div");return"draggable"in n||"dragDrop"in n}();function Mn(n){if(null==On){var t=D("span","​");O(n,D("span",[t,document.createTextNode("x")])),0!=n.firstChild.offsetHeight&&(On=t.offsetWidth<=1&&t.offsetHeight>2&&!(i&&l<8))}var e=On?D("span","​"):D("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return e.setAttribute("cm-text",""),e}function Pn(n){if(null!=Dn)return Dn;var t=O(n,document.createTextNode("AخA")),e=E(t,0,1).getBoundingClientRect(),a=E(t,1,2).getBoundingClientRect();return S(n),!(!e||e.left==e.right)&&(Dn=a.right-e.right<3)}var jn,Tn=3!="\n\nb".split(/\n/).length?function(n){for(var t=0,e=[],a=n.length;t<=a;){var r=n.indexOf("\n",t);-1==r&&(r=n.length);var o=n.slice(t,"\r"==n.charAt(r-1)?r-1:r),i=o.indexOf("\r");-1!=i?(e.push(o.slice(0,i)),t+=i+1):(e.push(o),t=r+1)}return e}:function(n){return n.split(/\r\n?|\n/)},Bn=window.getSelection?function(n){try{return n.selectionStart!=n.selectionEnd}catch(n){return!1}}:function(n){var t;try{t=n.ownerDocument.selection.createRange()}catch(n){}return!(!t||t.parentElement()!=n)&&0!=t.compareEndPoints("StartToEnd",t)},Ln="oncopy"in(jn=D("div"))||(jn.setAttribute("oncopy","return;"),"function"==typeof jn.oncopy),An=null,Nn={},Rn={};function Yn(n,t){arguments.length>2&&(t.dependencies=Array.prototype.slice.call(arguments,2)),Nn[n]=t}function Un(n){if("string"==typeof n&&Rn.hasOwnProperty(n))n=Rn[n];else if(n&&"string"==typeof n.name&&Rn.hasOwnProperty(n.name)){var t=Rn[n.name];"string"==typeof t&&(t={name:t}),(n=nn(t,n)).name=t.name}else{if("string"==typeof n&&/^[\w\-]+\/[\w\-]+\+xml$/.test(n))return Un("application/xml");if("string"==typeof n&&/^[\w\-]+\/[\w\-]+\+json$/.test(n))return Un("application/json")}return"string"==typeof n?{name:n}:n||{name:"null"}}function Hn(n,t){t=Un(t);var e=Nn[t.name];if(!e)return Hn(n,"text/plain");var a=e(n,t);if(Wn.hasOwnProperty(t.name)){var r=Wn[t.name];for(var o in r)r.hasOwnProperty(o)&&(a.hasOwnProperty(o)&&(a["_"+o]=a[o]),a[o]=r[o])}if(a.name=t.name,t.helperType&&(a.helperType=t.helperType),t.modeProps)for(var i in t.modeProps)a[i]=t.modeProps[i];return a}var Wn={};function Xn(n,t){R(t,Wn.hasOwnProperty(n)?Wn[n]:Wn[n]={})}function qn(n,t){if(!0===t)return t;if(n.copyState)return n.copyState(t);var e={};for(var a in t){var r=t[a];r instanceof Array&&(r=r.concat([])),e[a]=r}return e}function Zn(n,t){for(var e;n.innerMode&&(e=n.innerMode(t))&&e.mode!=n;)t=e.state,n=e.mode;return e||{mode:n,state:t}}function Kn(n,t,e){return!n.startState||n.startState(t,e)}var Vn=function(n,t,e){this.pos=this.start=0,this.string=n,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=e};function Gn(n,t){if((t-=n.first)<0||t>=n.size)throw new Error("There is no line "+(t+n.first)+" in the document.");for(var e=n;!e.lines;)for(var a=0;;++a){var r=e.children[a],o=r.chunkSize();if(t=n.first&&te?rt(e,Gn(n,e).text.length):function(n,t){var e=n.ch;return null==e||e>t?rt(n.line,t):e<0?rt(n.line,0):n}(t,Gn(n,t.line).text.length)}function bt(n,t){for(var e=[],a=0;a=this.string.length},Vn.prototype.sol=function(){return this.pos==this.lineStart},Vn.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},Vn.prototype.next=function(){if(this.post},Vn.prototype.eatSpace=function(){for(var n=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>n},Vn.prototype.skipToEnd=function(){this.pos=this.string.length},Vn.prototype.skipTo=function(n){var t=this.string.indexOf(n,this.pos);if(t>-1)return this.pos=t,!0},Vn.prototype.backUp=function(n){this.pos-=n},Vn.prototype.column=function(){return this.lastColumnPos0?null:(a&&!1!==t&&(this.pos+=a[0].length),a)}var r=function(n){return e?n.toLowerCase():n};if(r(this.string.substr(this.pos,n.length))==r(n))return!1!==t&&(this.pos+=n.length),!0},Vn.prototype.current=function(){return this.string.slice(this.start,this.pos)},Vn.prototype.hideFirstChars=function(n,t){this.lineStart+=n;try{return t()}finally{this.lineStart-=n}},Vn.prototype.lookAhead=function(n){var t=this.lineOracle;return t&&t.lookAhead(n)},Vn.prototype.baseToken=function(){var n=this.lineOracle;return n&&n.baseToken(this.pos)};var ut=function(n,t){this.state=n,this.lookAhead=t},ft=function(n,t,e,a){this.state=t,this.doc=n,this.line=e,this.maxLookAhead=a||0,this.baseTokens=null,this.baseTokenPos=1};function mt(n,t,e,a){var r=[n.state.modeGen],o={};_t(n,t.text,n.doc.mode,e,(function(n,t){return r.push(n,t)}),o,a);for(var i=e.state,l=function(a){e.baseTokens=r;var l=n.state.overlays[a],s=1,c=0;e.state=!0,_t(n,t.text,l.mode,e,(function(n,t){for(var e=s;cn&&r.splice(s,1,n,r[s+1],a),s+=2,c=Math.min(n,a)}if(t)if(l.opaque)r.splice(e,s-e,n,"overlay "+t),s=e+2;else for(;en.options.maxHighlightLength&&qn(n.doc.mode,a.state),o=mt(n,t,a);r&&(a.state=r),t.stateAfter=a.save(!r),t.styles=o.styles,o.classes?t.styleClasses=o.classes:t.styleClasses&&(t.styleClasses=null),e===n.doc.highlightFrontier&&(n.doc.modeFrontier=Math.max(n.doc.modeFrontier,++n.doc.highlightFrontier))}return t.styles}function gt(n,t,e){var a=n.doc,r=n.display;if(!a.mode.startState)return new ft(a,!0,t);var o=function(n,t,e){for(var a,r,o=n.doc,i=e?-1:t-(n.doc.mode.innerMode?1e3:100),l=t;l>i;--l){if(l<=o.first)return o.first;var s=Gn(o,l-1),c=s.stateAfter;if(c&&(!e||l+(c instanceof ut?c.lookAhead:0)<=o.modeFrontier))return l;var d=Y(s.text,null,n.options.tabSize);(null==r||a>d)&&(r=l-1,a=d)}return r}(n,t,e),i=o>a.first&&Gn(a,o-1).stateAfter,l=i?ft.fromSaved(a,i,o):new ft(a,Kn(a.mode),o);return a.iter(o,t,(function(e){xt(n,e.text,l);var a=l.line;e.stateAfter=a==t-1||a%5==0||a>=r.viewFrom&&at.start)return o}throw new Error("Mode "+n.name+" failed to advance stream.")}ft.prototype.lookAhead=function(n){var t=this.doc.getLine(this.line+n);return null!=t&&n>this.maxLookAhead&&(this.maxLookAhead=n),t},ft.prototype.baseToken=function(n){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=n;)this.baseTokenPos+=2;var t=this.baseTokens[this.baseTokenPos+1];return{type:t&&t.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-n}},ft.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},ft.fromSaved=function(n,t,e){return t instanceof ut?new ft(n,qn(n.mode,t.state),e,t.lookAhead):new ft(n,qn(n.mode,t),e)},ft.prototype.save=function(n){var t=!1!==n?qn(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new ut(t,this.maxLookAhead):t};var vt=function(n,t,e){this.start=n.start,this.end=n.pos,this.string=n.current(),this.type=t||null,this.state=e};function yt(n,t,e,a){var r,o,i=n.doc,l=i.mode,s=Gn(i,(t=pt(i,t)).line),c=gt(n,t.line,e),d=new Vn(s.text,n.options.tabSize,c);for(a&&(o=[]);(a||d.posn.options.maxHighlightLength?(l=!1,i&&xt(n,t,a,p.pos),p.pos=t.length,s=null):s=zt(kt(e,p,a.state,b),o),b){var u=b[0].name;u&&(s="m-"+(s?u+" "+s:u))}if(!l||d!=s){for(;c=t:o.to>t);(a||(a=[])).push(new Ct(i,o.from,l?null:o.to))}}return a}(e,r,i),s=function(n,t,e){var a;if(n)for(var r=0;r=t:o.to>t)||o.from==t&&"bookmark"==i.type&&(!e||o.marker.insertLeft)){var l=null==o.from||(i.inclusiveLeft?o.from<=t:o.from0&&l)for(var w=0;wt)&&(!e||Bt(e,o.marker)<0)&&(e=o.marker)}return e}function Yt(n,t,e,a,r){var o=Gn(n,t),i=Et&&o.markedSpans;if(i)for(var l=0;l=0&&p<=0||d<=0&&p>=0)&&(d<=0&&(s.marker.inclusiveRight&&r.inclusiveLeft?ot(c.to,e)>=0:ot(c.to,e)>0)||d>=0&&(s.marker.inclusiveRight&&r.inclusiveLeft?ot(c.from,a)<=0:ot(c.from,a)<0)))return!0}}}function Ut(n){for(var t;t=At(n);)n=t.find(-1,!0).line;return n}function Ht(n,t){var e=Gn(n,t),a=Ut(e);return e==a?t:nt(a)}function Wt(n,t){if(t>n.lastLine())return t;var e,a=Gn(n,t);if(!Xt(n,a))return t;for(;e=Nt(a);)a=e.find(1,!0).line;return nt(a)+1}function Xt(n,t){var e=Et&&t.markedSpans;if(e)for(var a=void 0,r=0;rt.maxLineLength&&(t.maxLineLength=e,t.maxLine=n)}))}var Gt=function(n,t,e){this.text=n,Pt(this,t),this.height=e?e(this):1};function $t(n){n.parent=null,Mt(n)}Gt.prototype.lineNo=function(){return nt(this)},yn(Gt);var Jt={},Qt={};function ne(n,t){if(!n||/^\s*$/.test(n))return null;var e=t.addModeClass?Qt:Jt;return e[n]||(e[n]=n.replace(/\S+/g,"cm-$&"))}function te(n,t){var e=I("span",null,null,s?"padding-right: .1px":null),a={pre:I("pre",[e],"CodeMirror-line"),content:e,col:0,pos:0,cm:n,trailingSpace:!1,splitSpaces:n.getOption("lineWrapping")};t.measure={};for(var r=0;r<=(t.rest?t.rest.length:0);r++){var o=r?t.rest[r-1]:t.line,i=void 0;a.pos=0,a.addToken=ae,Pn(n.display.measure)&&(i=un(o,n.doc.direction))&&(a.addToken=re(a.addToken,i)),a.map=[],ie(o,a,ht(n,o,t!=n.display.externalMeasured&&nt(o))),o.styleClasses&&(o.styleClasses.bgClass&&(a.bgClass=T(o.styleClasses.bgClass,a.bgClass||"")),o.styleClasses.textClass&&(a.textClass=T(o.styleClasses.textClass,a.textClass||""))),0==a.map.length&&a.map.push(0,0,a.content.appendChild(Mn(n.display.measure))),0==r?(t.measure.map=a.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(a.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(s){var l=a.content.lastChild;(/\bcm-tab\b/.test(l.className)||l.querySelector&&l.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return xn(n,"renderLine",n,t.line,a.pre),a.pre.className&&(a.textClass=T(a.pre.className,a.textClass||"")),a}function ee(n){var t=D("span","•","cm-invalidchar");return t.title="\\u"+n.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function ae(n,t,e,a,r,o,s){if(t){var c,d=n.splitSpaces?function(n,t){if(n.length>1&&!/ /.test(n))return n;for(var e=t,a="",r=0;rc&&p.from<=c);b++);if(p.to>=d)return n(e,a,r,o,i,l,s);n(e,a.slice(0,p.to-c),r,o,null,l,s),o=null,a=a.slice(p.to-c),c=p.to}}}function oe(n,t,e,a){var r=!a&&e.widgetNode;r&&n.map.push(n.pos,n.pos+t,r),!a&&n.cm.display.input.needsContentAttribute&&(r||(r=n.content.appendChild(document.createElement("span"))),r.setAttribute("cm-marker",e.id)),r&&(n.cm.display.input.setUneditable(r),n.content.appendChild(r)),n.pos+=t,n.trailingSpace=!1}function ie(n,t,e){var a=n.markedSpans,r=n.text,o=0;if(a)for(var i,l,s,c,d,p,b,u=r.length,f=0,m=1,h="",g=0;;){if(g==f){s=c=d=l="",b=null,p=null,g=1/0;for(var x=[],w=void 0,k=0;kf||y.collapsed&&v.to==f&&v.from==f)){if(null!=v.to&&v.to!=f&&g>v.to&&(g=v.to,c=""),y.className&&(s+=" "+y.className),y.css&&(l=(l?l+";":"")+y.css),y.startStyle&&v.from==f&&(d+=" "+y.startStyle),y.endStyle&&v.to==g&&(w||(w=[])).push(y.endStyle,v.to),y.title&&((b||(b={})).title=y.title),y.attributes)for(var z in y.attributes)(b||(b={}))[z]=y.attributes[z];y.collapsed&&(!p||Bt(p.marker,y)<0)&&(p=v)}else v.from>f&&g>v.from&&(g=v.from)}if(w)for(var _=0;_=u)break;for(var E=Math.min(u,g);;){if(h){var C=f+h.length;if(!p){var S=C>E?h.slice(0,E-f):h;t.addToken(t,S,i?i+s:s,d,f+S.length==g?c:"",l,b)}if(C>=E){h=h.slice(E-f),f=E;break}f=C,d=""}h=r.slice(o,o=e[m++]),i=ne(e[m++],t.cm.options)}}else for(var O=1;Oe)return{map:n.measure.maps[r],cache:n.measure.caches[r],before:!0}}}function Pe(n,t,e,a){return Be(n,Te(n,t),e,a)}function je(n,t){if(t>=n.display.viewFrom&&t=e.lineN&&t2&&o.push((s.bottom+c.top)/2-e.top)}}o.push(e.bottom-e.top)}}(n,t.view,t.rect),t.hasHeights=!0),(o=function(n,t,e,a){var r,o=Ne(t.map,e,a),s=o.node,c=o.start,d=o.end,p=o.collapse;if(3==s.nodeType){for(var b=0;b<4;b++){for(;c&&ln(t.line.text.charAt(o.coverStart+c));)--c;for(;o.coverStart+d1}(n))return t;var e=screen.logicalXDPI/screen.deviceXDPI,a=screen.logicalYDPI/screen.deviceYDPI;return{left:t.left*e,right:t.right*e,top:t.top*a,bottom:t.bottom*a}}(n.display.measure,r))}else{var u;c>0&&(p=a="right"),r=n.options.lineWrapping&&(u=s.getClientRects()).length>1?u["right"==a?u.length-1:0]:s.getBoundingClientRect()}if(i&&l<9&&!c&&(!r||!r.left&&!r.right)){var f=s.parentNode.getClientRects()[0];r=f?{left:f.left,right:f.left+la(n.display),top:f.top,bottom:f.bottom}:Ae}for(var m=r.top-t.rect.top,h=r.bottom-t.rect.top,g=(m+h)/2,x=t.view.measure.heights,w=0;wt)&&(r=(o=s-l)-1,t>=s&&(i="right")),null!=r){if(a=n[c+2],l==s&&e==(a.insertLeft?"left":"right")&&(i=e),"left"==e&&0==r)for(;c&&n[c-2]==n[c-3]&&n[c-1].insertLeft;)a=n[2+(c-=3)],i="left";if("right"==e&&r==s-l)for(;c=0&&(e=n[r]).left==e.right;r--);return e}function Ye(n){if(n.measure&&(n.measure.cache={},n.measure.heights=null,n.rest))for(var t=0;t=a.text.length?(s=a.text.length,c="before"):s<=0&&(s=0,c="after"),!l)return i("before"==c?s-1:s,"before"==c);function d(n,t,e){return i(e?n-1:n,1==l[t].level!=e)}var p=pn(l,s,c),b=dn,u=d(s,p,"before"==c);return null!=b&&(u.other=d(s,b,"before"!=c)),u}function $e(n,t){var e=0;t=pt(n.doc,t),n.options.lineWrapping||(e=la(n.display)*t.ch);var a=Gn(n.doc,t.line),r=Zt(a)+Ee(n.display);return{left:e,right:e,top:r,bottom:r+a.height}}function Je(n,t,e,a,r){var o=rt(n,t,e);return o.xRel=r,a&&(o.outside=a),o}function Qe(n,t,e){var a=n.doc;if((e+=n.display.viewOffset)<0)return Je(a.first,0,null,-1,-1);var r=tt(a,e),o=a.first+a.size-1;if(r>o)return Je(a.first+a.size-1,Gn(a,o).text.length,null,1,1);t<0&&(t=0);for(var i=Gn(a,r);;){var l=aa(n,i,r,t,e),s=Rt(i,l.ch+(l.xRel>0||l.outside>0?1:0));if(!s)return l;var c=s.find(1);if(c.line==r)return c;i=Gn(a,r=c.line)}}function na(n,t,e,a){a-=qe(t);var r=t.text.length,o=cn((function(t){return Be(n,e,t-1).bottom<=a}),r,0);return{begin:o,end:r=cn((function(t){return Be(n,e,t).top>a}),o,r)}}function ta(n,t,e,a){return e||(e=Te(n,t)),na(n,t,e,Ze(n,t,Be(n,e,a),"line").top)}function ea(n,t,e,a){return!(n.bottom<=e)&&(n.top>e||(a?n.left:n.right)>t)}function aa(n,t,e,a,r){r-=Zt(t);var o=Te(n,t),i=qe(t),l=0,s=t.text.length,c=!0,d=un(t,n.doc.direction);if(d){var p=(n.options.lineWrapping?oa:ra)(n,t,e,o,d,a,r);l=(c=1!=p.level)?p.from:p.to-1,s=c?p.to:p.from-1}var b,u,f=null,m=null,h=cn((function(t){var e=Be(n,o,t);return e.top+=i,e.bottom+=i,!!ea(e,a,r,!1)&&(e.top<=r&&e.left<=a&&(f=t,m=e),!0)}),l,s),g=!1;if(m){var x=a-m.left=k.bottom?1:0}return Je(e,h=sn(t.text,h,1),u,g,a-b)}function ra(n,t,e,a,r,o,i){var l=cn((function(l){var s=r[l],c=1!=s.level;return ea(Ge(n,rt(e,c?s.to:s.from,c?"before":"after"),"line",t,a),o,i,!0)}),0,r.length-1),s=r[l];if(l>0){var c=1!=s.level,d=Ge(n,rt(e,c?s.from:s.to,c?"after":"before"),"line",t,a);ea(d,o,i,!0)&&d.top>i&&(s=r[l-1])}return s}function oa(n,t,e,a,r,o,i){var l=na(n,t,a,i),s=l.begin,c=l.end;/\s/.test(t.text.charAt(c-1))&&c--;for(var d=null,p=null,b=0;b=c||u.to<=s)){var f=Be(n,a,1!=u.level?Math.min(c,u.to)-1:Math.max(s,u.from)).right,m=fm)&&(d=u,p=m)}}return d||(d=r[r.length-1]),d.fromc&&(d={from:d.from,to:c,level:d.level}),d}function ia(n){if(null!=n.cachedTextHeight)return n.cachedTextHeight;if(null==Le){Le=D("pre",null,"CodeMirror-line-like");for(var t=0;t<49;++t)Le.appendChild(document.createTextNode("x")),Le.appendChild(D("br"));Le.appendChild(document.createTextNode("x"))}O(n.measure,Le);var e=Le.offsetHeight/50;return e>3&&(n.cachedTextHeight=e),S(n.measure),e||1}function la(n){if(null!=n.cachedCharWidth)return n.cachedCharWidth;var t=D("span","xxxxxxxxxx"),e=D("pre",[t],"CodeMirror-line-like");O(n.measure,e);var a=t.getBoundingClientRect(),r=(a.right-a.left)/10;return r>2&&(n.cachedCharWidth=r),r||10}function sa(n){for(var t=n.display,e={},a={},r=t.gutters.clientLeft,o=t.gutters.firstChild,i=0;o;o=o.nextSibling,++i){var l=n.display.gutterSpecs[i].className;e[l]=o.offsetLeft+o.clientLeft+r,a[l]=o.clientWidth}return{fixedPos:ca(t),gutterTotalWidth:t.gutters.offsetWidth,gutterLeft:e,gutterWidth:a,wrapperWidth:t.wrapper.clientWidth}}function ca(n){return n.scroller.getBoundingClientRect().left-n.sizer.getBoundingClientRect().left}function da(n){var t=ia(n.display),e=n.options.lineWrapping,a=e&&Math.max(5,n.display.scroller.clientWidth/la(n.display)-3);return function(r){if(Xt(n.doc,r))return 0;var o=0;if(r.widgets)for(var i=0;i0&&(s=Gn(n.doc,c.line).text).length==c.ch){var d=Y(s,s.length,n.options.tabSize)-s.length;c=rt(c.line,Math.max(0,Math.round((o-Se(n.display).left)/la(n.display))-d))}return c}function ua(n,t){if(t>=n.display.viewTo)return null;if((t-=n.display.viewFrom)<0)return null;for(var e=n.display.view,a=0;at)&&(r.updateLineNumbers=t),n.curOp.viewChanged=!0,t>=r.viewTo)Et&&Ht(n.doc,t)r.viewFrom?ha(n):(r.viewFrom+=a,r.viewTo+=a);else if(t<=r.viewFrom&&e>=r.viewTo)ha(n);else if(t<=r.viewFrom){var o=ga(n,e,e+a,1);o?(r.view=r.view.slice(o.index),r.viewFrom=o.lineN,r.viewTo+=a):ha(n)}else if(e>=r.viewTo){var i=ga(n,t,t,-1);i?(r.view=r.view.slice(0,i.index),r.viewTo=i.lineN):ha(n)}else{var l=ga(n,t,t,-1),s=ga(n,e,e+a,1);l&&s?(r.view=r.view.slice(0,l.index).concat(se(n,l.lineN,s.lineN)).concat(r.view.slice(s.index)),r.viewTo+=a):ha(n)}var c=r.externalMeasured;c&&(e=r.lineN&&t=a.viewTo)){var o=a.view[ua(n,t)];if(null!=o.node){var i=o.changes||(o.changes=[]);-1==H(i,e)&&i.push(e)}}}function ha(n){n.display.viewFrom=n.display.viewTo=n.doc.first,n.display.view=[],n.display.viewOffset=0}function ga(n,t,e,a){var r,o=ua(n,t),i=n.display.view;if(!Et||e==n.doc.first+n.doc.size)return{index:o,lineN:e};for(var l=n.display.viewFrom,s=0;s0){if(o==i.length-1)return null;r=l+i[o].size-t,o++}else r=l-t;t+=r,e+=r}for(;Ht(n.doc,e)!=e;){if(o==(a<0?0:i.length-1))return null;e+=a*i[o-(a<0?1:0)].size,o+=a}return{index:o,lineN:e}}function xa(n){for(var t=n.display.view,e=0,a=0;a=n.display.viewTo||s.to().line0?i:n.defaultCharWidth())+"px"}if(a.other){var l=e.appendChild(D("div"," ","CodeMirror-cursor CodeMirror-secondarycursor"));l.style.display="",l.style.left=a.other.left+"px",l.style.top=a.other.top+"px",l.style.height=.85*(a.other.bottom-a.other.top)+"px"}}function ya(n,t){return n.top-t.top||n.left-t.left}function za(n,t,e){var a=n.display,r=n.doc,o=document.createDocumentFragment(),i=Se(n.display),l=i.left,s=Math.max(a.sizerWidth,De(n)-a.sizer.offsetLeft)-i.right,c="ltr"==r.direction;function d(n,t,e,a){t<0&&(t=0),t=Math.round(t),a=Math.round(a),o.appendChild(D("div",null,"CodeMirror-selected","position: absolute; left: "+n+"px;\n top: "+t+"px; width: "+(null==e?s-n:e)+"px;\n height: "+(a-t)+"px"))}function p(t,e,a){var o,i,p=Gn(r,t),b=p.text.length;function u(e,a){return Ve(n,rt(t,e),"div",p,a)}function f(t,e,a){var r=ta(n,p,null,t),o="ltr"==e==("after"==a)?"left":"right";return u("after"==a?r.begin:r.end-(/\s/.test(p.text.charAt(r.end-1))?2:1),o)[o]}var m=un(p,r.direction);return function(n,t,e,a){if(!n)return a(t,e,"ltr",0);for(var r=!1,o=0;ot||t==e&&i.to==t)&&(a(Math.max(i.from,t),Math.min(i.to,e),1==i.level?"rtl":"ltr",o),r=!0)}r||a(t,e,"ltr")}(m,e||0,null==a?b:a,(function(n,t,r,p){var h="ltr"==r,g=u(n,h?"left":"right"),x=u(t-1,h?"right":"left"),w=null==e&&0==n,k=null==a&&t==b,v=0==p,y=!m||p==m.length-1;if(x.top-g.top<=3){var z=(c?k:w)&&y,_=(c?w:k)&&v?l:(h?g:x).left,F=z?s:(h?x:g).right;d(_,g.top,F-_,g.bottom)}else{var E,C,S,O;h?(E=c&&w&&v?l:g.left,C=c?s:f(n,r,"before"),S=c?l:f(t,r,"after"),O=c&&k&&y?s:x.right):(E=c?f(n,r,"before"):l,C=!c&&w&&v?s:g.right,S=!c&&k&&y?l:x.left,O=c?f(t,r,"after"):s),d(E,g.top,C-E,g.bottom),g.bottom0?t.blinker=setInterval((function(){n.hasFocus()||Sa(n),t.cursorDiv.style.visibility=(e=!e)?"":"hidden"}),n.options.cursorBlinkRate):n.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function Fa(n){n.hasFocus()||(n.display.input.focus(),n.state.focused||Ca(n))}function Ea(n){n.state.delayingBlurEvent=!0,setTimeout((function(){n.state.delayingBlurEvent&&(n.state.delayingBlurEvent=!1,n.state.focused&&Sa(n))}),100)}function Ca(n,t){n.state.delayingBlurEvent&&!n.state.draggingText&&(n.state.delayingBlurEvent=!1),"nocursor"!=n.options.readOnly&&(n.state.focused||(xn(n,"focus",n,t),n.state.focused=!0,j(n.display.wrapper,"CodeMirror-focused"),n.curOp||n.display.selForContextMenu==n.doc.sel||(n.display.input.reset(),s&&setTimeout((function(){return n.display.input.reset(!0)}),20)),n.display.input.receivedFocus()),_a(n))}function Sa(n,t){n.state.delayingBlurEvent||(n.state.focused&&(xn(n,"blur",n,t),n.state.focused=!1,C(n.display.wrapper,"CodeMirror-focused")),clearInterval(n.display.blinker),setTimeout((function(){n.state.focused||(n.display.shift=!1)}),150))}function Oa(n){for(var t=n.display,e=t.lineDiv.offsetTop,a=Math.max(0,t.scroller.getBoundingClientRect().top),r=t.lineDiv.getBoundingClientRect().top,o=0,s=0;s.005||m<-.005)&&(rn.display.sizerWidth){var g=Math.ceil(b/la(n.display));g>n.display.maxLineLength&&(n.display.maxLineLength=g,n.display.maxLine=c.line,n.display.maxLineChanged=!0)}}}Math.abs(o)>2&&(t.scroller.scrollTop+=o)}function Da(n){if(n.widgets)for(var t=0;t=i&&(o=tt(t,Zt(Gn(t,s))-n.wrapper.clientHeight),i=s)}return{from:o,to:Math.max(i,o+1)}}function Ma(n,t){var e=n.display,a=ia(n.display);t.top<0&&(t.top=0);var r=n.curOp&&null!=n.curOp.scrollTop?n.curOp.scrollTop:e.scroller.scrollTop,o=Ie(n),i={};t.bottom-t.top>o&&(t.bottom=t.top+o);var l=n.doc.height+Ce(e),s=t.topl-a;if(t.topr+o){var d=Math.min(t.top,(c?l:t.bottom)-o);d!=r&&(i.scrollTop=d)}var p=n.options.fixedGutter?0:e.gutters.offsetWidth,b=n.curOp&&null!=n.curOp.scrollLeft?n.curOp.scrollLeft:e.scroller.scrollLeft-p,u=De(n)-e.gutters.offsetWidth,f=t.right-t.left>u;return f&&(t.right=t.left+u),t.left<10?i.scrollLeft=0:t.leftu+b-3&&(i.scrollLeft=t.right+(f?0:10)-u),i}function Pa(n,t){null!=t&&(Ba(n),n.curOp.scrollTop=(null==n.curOp.scrollTop?n.doc.scrollTop:n.curOp.scrollTop)+t)}function ja(n){Ba(n);var t=n.getCursor();n.curOp.scrollToPos={from:t,to:t,margin:n.options.cursorScrollMargin}}function Ta(n,t,e){null==t&&null==e||Ba(n),null!=t&&(n.curOp.scrollLeft=t),null!=e&&(n.curOp.scrollTop=e)}function Ba(n){var t=n.curOp.scrollToPos;t&&(n.curOp.scrollToPos=null,La(n,$e(n,t.from),$e(n,t.to),t.margin))}function La(n,t,e,a){var r=Ma(n,{left:Math.min(t.left,e.left),top:Math.min(t.top,e.top)-a,right:Math.max(t.right,e.right),bottom:Math.max(t.bottom,e.bottom)+a});Ta(n,r.scrollLeft,r.scrollTop)}function Aa(n,t){Math.abs(n.doc.scrollTop-t)<2||(e||br(n,{top:t}),Na(n,t,!0),e&&br(n),ir(n,100))}function Na(n,t,e){t=Math.max(0,Math.min(n.display.scroller.scrollHeight-n.display.scroller.clientHeight,t)),(n.display.scroller.scrollTop!=t||e)&&(n.doc.scrollTop=t,n.display.scrollbars.setScrollTop(t),n.display.scroller.scrollTop!=t&&(n.display.scroller.scrollTop=t))}function Ra(n,t,e,a){t=Math.max(0,Math.min(t,n.display.scroller.scrollWidth-n.display.scroller.clientWidth)),(e?t==n.doc.scrollLeft:Math.abs(n.doc.scrollLeft-t)<2)&&!a||(n.doc.scrollLeft=t,mr(n),n.display.scroller.scrollLeft!=t&&(n.display.scroller.scrollLeft=t),n.display.scrollbars.setScrollLeft(t))}function Ya(n){var t=n.display,e=t.gutters.offsetWidth,a=Math.round(n.doc.height+Ce(n.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:n.options.fixedGutter?e:0,docHeight:a,scrollHeight:a+Oe(n)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:e}}var Ua=function(n,t,e){this.cm=e;var a=this.vert=D("div",[D("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),r=this.horiz=D("div",[D("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=r.tabIndex=-1,n(a),n(r),mn(a,"scroll",(function(){a.clientHeight&&t(a.scrollTop,"vertical")})),mn(r,"scroll",(function(){r.clientWidth&&t(r.scrollLeft,"horizontal")})),this.checkedZeroWidth=!1,i&&l<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};Ua.prototype.update=function(n){var t=n.scrollWidth>n.clientWidth+1,e=n.scrollHeight>n.clientHeight+1,a=n.nativeBarWidth;if(e){this.vert.style.display="block",this.vert.style.bottom=t?a+"px":"0";var r=n.viewHeight-(t?a:0);this.vert.firstChild.style.height=Math.max(0,n.scrollHeight-n.clientHeight+r)+"px"}else this.vert.scrollTop=0,this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=e?a+"px":"0",this.horiz.style.left=n.barLeft+"px";var o=n.viewWidth-n.barLeft-(e?a:0);this.horiz.firstChild.style.width=Math.max(0,n.scrollWidth-n.clientWidth+o)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&n.clientHeight>0&&(0==a&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:e?a:0,bottom:t?a:0}},Ua.prototype.setScrollLeft=function(n){this.horiz.scrollLeft!=n&&(this.horiz.scrollLeft=n),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Ua.prototype.setScrollTop=function(n){this.vert.scrollTop!=n&&(this.vert.scrollTop=n),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Ua.prototype.zeroWidthHack=function(){var n=w&&!f?"12px":"18px";this.horiz.style.height=this.vert.style.width=n,this.horiz.style.visibility=this.vert.style.visibility="hidden",this.disableHoriz=new U,this.disableVert=new U},Ua.prototype.enableZeroWidthBar=function(n,t,e){n.style.visibility="",t.set(1e3,(function a(){var r=n.getBoundingClientRect();("vert"==e?document.elementFromPoint(r.right-1,(r.top+r.bottom)/2):document.elementFromPoint((r.right+r.left)/2,r.bottom-1))!=n?n.style.visibility="hidden":t.set(1e3,a)}))},Ua.prototype.clear=function(){var n=this.horiz.parentNode;n.removeChild(this.horiz),n.removeChild(this.vert)};var Ha=function(){};function Wa(n,t){t||(t=Ya(n));var e=n.display.barWidth,a=n.display.barHeight;Xa(n,t);for(var r=0;r<4&&e!=n.display.barWidth||a!=n.display.barHeight;r++)e!=n.display.barWidth&&n.options.lineWrapping&&Oa(n),Xa(n,Ya(n)),e=n.display.barWidth,a=n.display.barHeight}function Xa(n,t){var e=n.display,a=e.scrollbars.update(t);e.sizer.style.paddingRight=(e.barWidth=a.right)+"px",e.sizer.style.paddingBottom=(e.barHeight=a.bottom)+"px",e.heightForcer.style.borderBottom=a.bottom+"px solid transparent",a.right&&a.bottom?(e.scrollbarFiller.style.display="block",e.scrollbarFiller.style.height=a.bottom+"px",e.scrollbarFiller.style.width=a.right+"px"):e.scrollbarFiller.style.display="",a.bottom&&n.options.coverGutterNextToScrollbar&&n.options.fixedGutter?(e.gutterFiller.style.display="block",e.gutterFiller.style.height=a.bottom+"px",e.gutterFiller.style.width=t.gutterWidth+"px"):e.gutterFiller.style.display=""}Ha.prototype.update=function(){return{bottom:0,right:0}},Ha.prototype.setScrollLeft=function(){},Ha.prototype.setScrollTop=function(){},Ha.prototype.clear=function(){};var qa={native:Ua,null:Ha};function Za(n){n.display.scrollbars&&(n.display.scrollbars.clear(),n.display.scrollbars.addClass&&C(n.display.wrapper,n.display.scrollbars.addClass)),n.display.scrollbars=new qa[n.options.scrollbarStyle]((function(t){n.display.wrapper.insertBefore(t,n.display.scrollbarFiller),mn(t,"mousedown",(function(){n.state.focused&&setTimeout((function(){return n.display.input.focus()}),0)})),t.setAttribute("cm-not-content","true")}),(function(t,e){"horizontal"==e?Ra(n,t):Aa(n,t)}),n),n.display.scrollbars.addClass&&j(n.display.wrapper,n.display.scrollbars.addClass)}var Ka=0;function Va(n){var t;n.curOp={cm:n,viewChanged:!1,startHeight:n.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Ka,markArrays:null},t=n.curOp,ce?ce.ops.push(t):t.ownsGroup=ce={ops:[t],delayedCallbacks:[]}}function Ga(n){var t=n.curOp;t&&function(n,t){var e=n.ownsGroup;if(e)try{!function(n){var t=n.delayedCallbacks,e=0;do{for(;e=e.viewTo)||e.maxLineChanged&&t.options.lineWrapping,n.update=n.mustUpdate&&new sr(t,n.mustUpdate&&{top:n.scrollTop,ensure:n.scrollToPos},n.forceUpdate)}function Ja(n){n.updatedDisplay=n.mustUpdate&&dr(n.cm,n.update)}function Qa(n){var t=n.cm,e=t.display;n.updatedDisplay&&Oa(t),n.barMeasure=Ya(t),e.maxLineChanged&&!t.options.lineWrapping&&(n.adjustWidthTo=Pe(t,e.maxLine,e.maxLine.text.length).left+3,t.display.sizerWidth=n.adjustWidthTo,n.barMeasure.scrollWidth=Math.max(e.scroller.clientWidth,e.sizer.offsetLeft+n.adjustWidthTo+Oe(t)+t.display.barWidth),n.maxScrollLeft=Math.max(0,e.sizer.offsetLeft+n.adjustWidthTo-De(t))),(n.updatedDisplay||n.selectionChanged)&&(n.preparedSelection=e.input.prepareSelection())}function nr(n){var t=n.cm;null!=n.adjustWidthTo&&(t.display.sizer.style.minWidth=n.adjustWidthTo+"px",n.maxScrollLeft(o.defaultView.innerHeight||o.documentElement.clientHeight)&&(r=!1),null!=r&&!m){var i=D("div","​",null,"position: absolute;\n top: "+(t.top-e.viewOffset-Ee(n.display))+"px;\n height: "+(t.bottom-t.top+Oe(n)+e.barHeight)+"px;\n left: "+t.left+"px; width: "+Math.max(2,t.right-t.left)+"px;");n.display.lineSpace.appendChild(i),i.scrollIntoView(r),n.display.lineSpace.removeChild(i)}}}(t,function(n,t,e,a){var r;null==a&&(a=0),n.options.lineWrapping||t!=e||(e="before"==t.sticky?rt(t.line,t.ch+1,"before"):t,t=t.ch?rt(t.line,"before"==t.sticky?t.ch-1:t.ch,"after"):t);for(var o=0;o<5;o++){var i=!1,l=Ge(n,t),s=e&&e!=t?Ge(n,e):l,c=Ma(n,r={left:Math.min(l.left,s.left),top:Math.min(l.top,s.top)-a,right:Math.max(l.left,s.left),bottom:Math.max(l.bottom,s.bottom)+a}),d=n.doc.scrollTop,p=n.doc.scrollLeft;if(null!=c.scrollTop&&(Aa(n,c.scrollTop),Math.abs(n.doc.scrollTop-d)>1&&(i=!0)),null!=c.scrollLeft&&(Ra(n,c.scrollLeft),Math.abs(n.doc.scrollLeft-p)>1&&(i=!0)),!i)break}return r}(t,pt(a,n.scrollToPos.from),pt(a,n.scrollToPos.to),n.scrollToPos.margin));var r=n.maybeHiddenMarkers,o=n.maybeUnhiddenMarkers;if(r)for(var i=0;i=n.display.viewTo)){var e=+new Date+n.options.workTime,a=gt(n,t.highlightFrontier),r=[];t.iter(a.line,Math.min(t.first+t.size,n.display.viewTo+500),(function(o){if(a.line>=n.display.viewFrom){var i=o.styles,l=o.text.length>n.options.maxHighlightLength?qn(t.mode,a.state):null,s=mt(n,o,a,!0);l&&(a.state=l),o.styles=s.styles;var c=o.styleClasses,d=s.classes;d?o.styleClasses=d:c&&(o.styleClasses=null);for(var p=!i||i.length!=o.styles.length||c!=d&&(!c||!d||c.bgClass!=d.bgClass||c.textClass!=d.textClass),b=0;!p&&be)return ir(n,n.options.workDelay),!0})),t.highlightFrontier=a.line,t.modeFrontier=Math.max(t.modeFrontier,a.line),r.length&&er(n,(function(){for(var t=0;t=e.viewFrom&&t.visible.to<=e.viewTo&&(null==e.updateLineNumbers||e.updateLineNumbers>=e.viewTo)&&e.renderedView==e.view&&0==xa(n))return!1;hr(n)&&(ha(n),t.dims=sa(n));var r=a.first+a.size,o=Math.max(t.visible.from-n.options.viewportMargin,a.first),i=Math.min(r,t.visible.to+n.options.viewportMargin);e.viewFromi&&e.viewTo-i<20&&(i=Math.min(r,e.viewTo)),Et&&(o=Ht(n.doc,o),i=Wt(n.doc,i));var l=o!=e.viewFrom||i!=e.viewTo||e.lastWrapHeight!=t.wrapperHeight||e.lastWrapWidth!=t.wrapperWidth;!function(n,t,e){var a=n.display;0==a.view.length||t>=a.viewTo||e<=a.viewFrom?(a.view=se(n,t,e),a.viewFrom=t):(a.viewFrom>t?a.view=se(n,t,a.viewFrom).concat(a.view):a.viewFrome&&(a.view=a.view.slice(0,ua(n,e)))),a.viewTo=e}(n,o,i),e.viewOffset=Zt(Gn(n.doc,e.viewFrom)),n.display.mover.style.top=e.viewOffset+"px";var c=xa(n);if(!l&&0==c&&!t.force&&e.renderedView==e.view&&(null==e.updateLineNumbers||e.updateLineNumbers>=e.viewTo))return!1;var d=cr(n);return c>4&&(e.lineDiv.style.display="none"),function(n,t,e){var a=n.display,r=n.options.lineNumbers,o=a.lineDiv,i=o.firstChild;function l(t){var e=t.nextSibling;return s&&w&&n.display.currentWheelTarget==t?t.style.display="none":t.parentNode.removeChild(t),e}for(var c=a.view,d=a.viewFrom,p=0;p-1&&(u=!1),ue(n,b,d,e)),u&&(S(b.lineNumber),b.lineNumber.appendChild(document.createTextNode(at(n.options,d)))),i=b.node.nextSibling}else{var f=ke(n,b,d,e);o.insertBefore(f,i)}d+=b.size}for(;i;)i=l(i)}(n,e.updateLineNumbers,t.dims),c>4&&(e.lineDiv.style.display=""),e.renderedView=e.view,function(n){if(n&&n.activeElt&&n.activeElt!=P(n.activeElt.ownerDocument)&&(n.activeElt.focus(),!/^(INPUT|TEXTAREA)$/.test(n.activeElt.nodeName)&&n.anchorNode&&M(document.body,n.anchorNode)&&M(document.body,n.focusNode))){var t=n.activeElt.ownerDocument,e=t.defaultView.getSelection(),a=t.createRange();a.setEnd(n.anchorNode,n.anchorOffset),a.collapse(!1),e.removeAllRanges(),e.addRange(a),e.extend(n.focusNode,n.focusOffset)}}(d),S(e.cursorDiv),S(e.selectionDiv),e.gutters.style.height=e.sizer.style.minHeight=0,l&&(e.lastWrapHeight=t.wrapperHeight,e.lastWrapWidth=t.wrapperWidth,ir(n,400)),e.updateLineNumbers=null,!0}function pr(n,t){for(var e=t.viewport,a=!0;;a=!1){if(a&&n.options.lineWrapping&&t.oldDisplayWidth!=De(n))a&&(t.visible=Ia(n.display,n.doc,e));else if(e&&null!=e.top&&(e={top:Math.min(n.doc.height+Ce(n.display)-Ie(n),e.top)}),t.visible=Ia(n.display,n.doc,e),t.visible.from>=n.display.viewFrom&&t.visible.to<=n.display.viewTo)break;if(!dr(n,t))break;Oa(n);var r=Ya(n);wa(n),Wa(n,r),fr(n,r),t.force=!1}t.signal(n,"update",n),n.display.viewFrom==n.display.reportedViewFrom&&n.display.viewTo==n.display.reportedViewTo||(t.signal(n,"viewportChange",n,n.display.viewFrom,n.display.viewTo),n.display.reportedViewFrom=n.display.viewFrom,n.display.reportedViewTo=n.display.viewTo)}function br(n,t){var e=new sr(n,t);if(dr(n,e)){Oa(n),pr(n,e);var a=Ya(n);wa(n),Wa(n,a),fr(n,a),e.finish()}}function ur(n){var t=n.gutters.offsetWidth;n.sizer.style.marginLeft=t+"px",pe(n,"gutterChanged",n)}function fr(n,t){n.display.sizer.style.minHeight=t.docHeight+"px",n.display.heightForcer.style.top=t.docHeight+"px",n.display.gutters.style.height=t.docHeight+n.display.barHeight+Oe(n)+"px"}function mr(n){var t=n.display,e=t.view;if(t.alignWidgets||t.gutters.firstChild&&n.options.fixedGutter){for(var a=ca(t)-t.scroller.scrollLeft+n.doc.scrollLeft,r=t.gutters.offsetWidth,o=a+"px",i=0;i=105&&(o.wrapper.style.clipPath="inset(0px)"),o.wrapper.setAttribute("translate","no"),i&&l<8&&(o.gutters.style.zIndex=-1,o.scroller.style.paddingRight=0),s||e&&x||(o.scroller.draggable=!0),n&&(n.appendChild?n.appendChild(o.wrapper):n(o.wrapper)),o.viewFrom=o.viewTo=t.first,o.reportedViewFrom=o.reportedViewTo=t.first,o.view=[],o.renderedView=null,o.externalMeasured=null,o.viewOffset=0,o.lastWrapHeight=o.lastWrapWidth=0,o.updateLineNumbers=null,o.nativeBarWidth=o.barHeight=o.barWidth=0,o.scrollbarsClipped=!1,o.lineNumWidth=o.lineNumInnerWidth=o.lineNumChars=null,o.alignWidgets=!1,o.cachedCharWidth=o.cachedTextHeight=o.cachedPaddingH=null,o.maxLine=null,o.maxLineLength=0,o.maxLineChanged=!1,o.wheelDX=o.wheelDY=o.wheelStartX=o.wheelStartY=null,o.shift=!1,o.selForContextMenu=null,o.activeTouch=null,o.gutterSpecs=gr(r.gutters,r.lineNumbers),xr(o),a.init(o)}sr.prototype.signal=function(n,t){vn(n,t)&&this.events.push(arguments)},sr.prototype.finish=function(){for(var n=0;nc.clientWidth,f=c.scrollHeight>c.clientHeight;if(r&&u||o&&f){if(o&&w&&s)n:for(var m=t.target,h=l.view;m!=c;m=m.parentNode)for(var g=0;g=0&&ot(n,a.to())<=0)return e}return-1};var Cr=function(n,t){this.anchor=n,this.head=t};function Sr(n,t,e){var a=n&&n.options.selectionsMayTouch,r=t[e];t.sort((function(n,t){return ot(n.from(),t.from())})),e=H(t,r);for(var o=1;o0:s>=0){var c=ct(l.from(),i.from()),d=st(l.to(),i.to()),p=l.empty()?i.from()==i.head:l.from()==l.head;o<=e&&--e,t.splice(--o,2,new Cr(p?d:c,p?c:d))}}return new Er(t,e)}function Or(n,t){return new Er([new Cr(n,t||n)],0)}function Dr(n){return n.text?rt(n.from.line+n.text.length-1,$(n.text).length+(1==n.text.length?n.from.ch:0)):n.to}function Ir(n,t){if(ot(n,t.from)<0)return n;if(ot(n,t.to)<=0)return Dr(t);var e=n.line+t.text.length-(t.to.line-t.from.line)-1,a=n.ch;return n.line==t.to.line&&(a+=Dr(t).ch-t.to.ch),rt(e,a)}function Mr(n,t){for(var e=[],a=0;a1&&n.remove(l.line+1,f-1),n.insert(l.line+1,g)}pe(n,"change",n,t)}function Ar(n,t,e){!function n(a,r,o){if(a.linked)for(var i=0;il-(n.cm?n.cm.options.historyEventDelay:500)||"*"==t.origin.charAt(0)))&&(o=function(n,t){return t?(Hr(n.done),$(n.done)):n.done.length&&!$(n.done).ranges?$(n.done):n.done.length>1&&!n.done[n.done.length-2].ranges?(n.done.pop(),$(n.done)):void 0}(r,r.lastOp==a)))i=$(o.changes),0==ot(t.from,t.to)&&0==ot(t.from,i.to)?i.to=Dr(t):o.changes.push(Ur(n,t));else{var s=$(r.done);for(s&&s.ranges||qr(n.sel,r.done),o={changes:[Ur(n,t)],generation:r.generation},r.done.push(o);r.done.length>r.undoDepth;)r.done.shift(),r.done[0].ranges||r.done.shift()}r.done.push(e),r.generation=++r.maxGeneration,r.lastModTime=r.lastSelTime=l,r.lastOp=r.lastSelOp=a,r.lastOrigin=r.lastSelOrigin=t.origin,i||xn(n,"historyAdded")}function Xr(n,t,e,a){var r=n.history,o=a&&a.origin;e==r.lastSelOp||o&&r.lastSelOrigin==o&&(r.lastModTime==r.lastSelTime&&r.lastOrigin==o||function(n,t,e,a){var r=t.charAt(0);return"*"==r||"+"==r&&e.ranges.length==a.ranges.length&&e.somethingSelected()==a.somethingSelected()&&new Date-n.history.lastSelTime<=(n.cm?n.cm.options.historyEventDelay:500)}(n,o,$(r.done),t))?r.done[r.done.length-1]=t:qr(t,r.done),r.lastSelTime=+new Date,r.lastSelOrigin=o,r.lastSelOp=e,a&&!1!==a.clearRedo&&Hr(r.undone)}function qr(n,t){var e=$(t);e&&e.ranges&&e.equals(n)||t.push(n)}function Zr(n,t,e,a){var r=t["spans_"+n.id],o=0;n.iter(Math.max(n.first,e),Math.min(n.first+n.size,a),(function(e){e.markedSpans&&((r||(r=t["spans_"+n.id]={}))[o]=e.markedSpans),++o}))}function Kr(n){if(!n)return null;for(var t,e=0;e-1&&($(l)[p]=c[p],delete c[p])}}}return a}function $r(n,t,e,a){if(a){var r=n.anchor;if(e){var o=ot(t,r)<0;o!=ot(e,r)<0?(r=t,t=e):o!=ot(t,e)<0&&(t=e)}return new Cr(r,t)}return new Cr(e||t,t)}function Jr(n,t,e,a,r){null==r&&(r=n.cm&&(n.cm.display.shift||n.extend)),ao(n,new Er([$r(n.sel.primary(),t,e,r)],0),a)}function Qr(n,t,e){for(var a=[],r=n.cm&&(n.cm.display.shift||n.extend),o=0;o=t.ch:l.to>t.ch))){if(r&&(xn(s,"beforeCursorEnter"),s.explicitlyCleared)){if(o.markedSpans){--i;continue}break}if(!s.atomic)continue;if(e){var p=s.find(a<0?1:-1),b=void 0;if((a<0?d:c)&&(p=po(n,p,-a,p&&p.line==t.line?o:null)),p&&p.line==t.line&&(b=ot(p,e))&&(a<0?b<0:b>0))return so(n,p,t,a,r)}var u=s.find(a<0?-1:1);return(a<0?c:d)&&(u=po(n,u,a,u.line==t.line?o:null)),u?so(n,u,t,a,r):null}}return t}function co(n,t,e,a,r){var o=a||1,i=so(n,t,e,o,r)||!r&&so(n,t,e,o,!0)||so(n,t,e,-o,r)||!r&&so(n,t,e,-o,!0);return i||(n.cantEdit=!0,rt(n.first,0))}function po(n,t,e,a){return e<0&&0==t.ch?t.line>n.first?pt(n,rt(t.line-1)):null:e>0&&t.ch==(a||Gn(n,t.line)).text.length?t.line0)){var d=[s,1],p=ot(c.from,l.from),b=ot(c.to,l.to);(p<0||!i.inclusiveLeft&&!p)&&d.push({from:c.from,to:l.from}),(b>0||!i.inclusiveRight&&!b)&&d.push({from:l.to,to:c.to}),r.splice.apply(r,d),s+=d.length-3}}return r}(n,t.from,t.to);if(a)for(var r=a.length-1;r>=0;--r)mo(n,{from:a[r].from,to:a[r].to,text:r?[""]:t.text,origin:t.origin});else mo(n,t)}}function mo(n,t){if(1!=t.text.length||""!=t.text[0]||0!=ot(t.from,t.to)){var e=Mr(n,t);Wr(n,t,e,n.cm?n.cm.curOp.id:NaN),xo(n,t,e,Dt(n,t));var a=[];Ar(n,(function(n,e){e||-1!=H(a,n.history)||(yo(n.history,t),a.push(n.history)),xo(n,t,null,Dt(n,t))}))}}function ho(n,t,e){var a=n.cm&&n.cm.state.suppressEdits;if(!a||e){for(var r,o=n.history,i=n.sel,l="undo"==t?o.done:o.undone,s="undo"==t?o.undone:o.done,c=0;c=0;--u){var f=b(u);if(f)return f.v}}}}function go(n,t){if(0!=t&&(n.first+=t,n.sel=new Er(J(n.sel.ranges,(function(n){return new Cr(rt(n.anchor.line+t,n.anchor.ch),rt(n.head.line+t,n.head.ch))})),n.sel.primIndex),n.cm)){fa(n.cm,n.first,n.first-t,t);for(var e=n.cm.display,a=e.viewFrom;an.lastLine())){if(t.from.lineo&&(t={from:t.from,to:rt(o,Gn(n,o).text.length),text:[t.text[0]],origin:t.origin}),t.removed=$n(n,t.from,t.to),e||(e=Mr(n,t)),n.cm?function(n,t,e){var a=n.doc,r=n.display,o=t.from,i=t.to,l=!1,s=o.line;n.options.lineWrapping||(s=nt(Ut(Gn(a,o.line))),a.iter(s,i.line+1,(function(n){if(n==r.maxLine)return l=!0,!0}))),a.sel.contains(t.from,t.to)>-1&&kn(n),Lr(a,t,e,da(n)),n.options.lineWrapping||(a.iter(s,o.line+t.text.length,(function(n){var t=Kt(n);t>r.maxLineLength&&(r.maxLine=n,r.maxLineLength=t,r.maxLineChanged=!0,l=!1)})),l&&(n.curOp.updateMaxLine=!0)),function(n,t){if(n.modeFrontier=Math.min(n.modeFrontier,t),!(n.highlightFrontiere;a--){var r=Gn(n,a).stateAfter;if(r&&(!(r instanceof ut)||a+r.lookAhead1||!(this.children[0]instanceof _o))){var l=[];this.collapse(l),this.children=[new _o(l)],this.children[0].parent=this}},collapse:function(n){for(var t=0;t50){for(var i=r.lines.length%25+25,l=i;l10);n.parent.maybeSpill()}},iterN:function(n,t,e){for(var a=0;a0||0==i&&!1!==o.clearWhenEmpty)return o;if(o.replacedWith&&(o.collapsed=!0,o.widgetNode=I("span",[o.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||o.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(o.widgetNode.insertLeft=!0)),o.collapsed){if(Yt(n,t.line,t,e,o)||t.line!=e.line&&Yt(n,e.line,t,e,o))throw new Error("Inserting collapsed marker partially overlapping an existing one");Et=!0}o.addToHistory&&Wr(n,{from:t,to:e,origin:"markText"},n.sel,NaN);var l,s=t.line,c=n.cm;if(n.iter(s,e.line+1,(function(a){c&&o.collapsed&&!c.options.lineWrapping&&Ut(a)==c.display.maxLine&&(l=!0),o.collapsed&&s!=t.line&&Qn(a,0),function(n,t,e){var a=e&&window.WeakSet&&(e.markedSpans||(e.markedSpans=new WeakSet));a&&n.markedSpans&&a.has(n.markedSpans)?n.markedSpans.push(t):(n.markedSpans=n.markedSpans?n.markedSpans.concat([t]):[t],a&&a.add(n.markedSpans)),t.marker.attachLine(n)}(a,new Ct(o,s==t.line?t.ch:null,s==e.line?e.ch:null),n.cm&&n.cm.curOp),++s})),o.collapsed&&n.iter(t.line,e.line+1,(function(t){Xt(n,t)&&Qn(t,0)})),o.clearOnEnter&&mn(o,"beforeCursorEnter",(function(){return o.clear()})),o.readOnly&&(Ft=!0,(n.history.done.length||n.history.undone.length)&&n.clearHistory()),o.collapsed&&(o.id=++So,o.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),o.collapsed)fa(c,t.line,e.line+1);else if(o.className||o.startStyle||o.endStyle||o.css||o.attributes||o.title)for(var d=t.line;d<=e.line;d++)ma(c,d,"text");o.atomic&&io(c.doc),pe(c,"markerAdded",c,o)}return o}Oo.prototype.clear=function(){if(!this.explicitlyCleared){var n=this.doc.cm,t=n&&!n.curOp;if(t&&Va(n),vn(this,"clear")){var e=this.find();e&&pe(this,"clear",e.from,e.to)}for(var a=null,r=null,o=0;on.display.maxLineLength&&(n.display.maxLine=c,n.display.maxLineLength=d,n.display.maxLineChanged=!0)}null!=a&&n&&this.collapsed&&fa(n,a,r+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,n&&io(n.doc)),n&&pe(n,"markerCleared",n,this,a,r),t&&Ga(n),this.parent&&this.parent.clear()}},Oo.prototype.find=function(n,t){var e,a;null==n&&"bookmark"==this.type&&(n=1);for(var r=0;r=0;s--)fo(this,a[s]);l?eo(this,l):this.cm&&ja(this.cm)})),undo:or((function(){ho(this,"undo")})),redo:or((function(){ho(this,"redo")})),undoSelection:or((function(){ho(this,"undo",!0)})),redoSelection:or((function(){ho(this,"redo",!0)})),setExtending:function(n){this.extend=n},getExtending:function(){return this.extend},historySize:function(){for(var n=this.history,t=0,e=0,a=0;a=n.ch)&&t.push(r.marker.parent||r.marker)}return t},findMarks:function(n,t,e){n=pt(this,n),t=pt(this,t);var a=[],r=n.line;return this.iter(n.line,t.line+1,(function(o){var i=o.markedSpans;if(i)for(var l=0;l=s.to||null==s.from&&r!=n.line||null!=s.from&&r==t.line&&s.from>=t.ch||e&&!e(s.marker)||a.push(s.marker.parent||s.marker)}++r})),a},getAllMarks:function(){var n=[];return this.iter((function(t){var e=t.markedSpans;if(e)for(var a=0;an)return t=n,!0;n-=o,++e})),pt(this,rt(e,t))},indexFromPos:function(n){var t=(n=pt(this,n)).ch;if(n.linet&&(t=n.from),null!=n.to&&n.to-1)return t.state.draggingText(n),void setTimeout((function(){return t.display.input.focus()}),20);try{var p=n.dataTransfer.getData("Text");if(p){var b;if(t.state.draggingText&&!t.state.draggingText.copy&&(b=t.listSelections()),ro(t.doc,Or(e,e)),b)for(var u=0;u=0;t--)wo(n.doc,"",a[t].from,a[t].to,"+delete");ja(n)}))}function ei(n,t,e){var a=sn(n.text,t+e,e);return a<0||a>n.text.length?null:a}function ai(n,t,e){var a=ei(n,t.ch,e);return null==a?null:new rt(t.line,a,e<0?"after":"before")}function ri(n,t,e,a,r){if(n){"rtl"==t.doc.direction&&(r=-r);var o=un(e,t.doc.direction);if(o){var i,l=r<0?$(o):o[0],s=r<0==(1==l.level)?"after":"before";if(l.level>0||"rtl"==t.doc.direction){var c=Te(t,e);i=r<0?e.text.length-1:0;var d=Be(t,c,i).top;i=cn((function(n){return Be(t,c,n).top==d}),r<0==(1==l.level)?l.from:l.to-1,i),"before"==s&&(i=ei(e,i,1))}else i=r<0?l.to:l.from;return new rt(a,i,s)}}return new rt(a,r<0?e.text.length:0,r<0?"before":"after")}Zo.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},Zo.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},Zo.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},Zo.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},Zo.default=w?Zo.macDefault:Zo.pcDefault;var oi={selectAll:bo,singleSelection:function(n){return n.setSelection(n.getCursor("anchor"),n.getCursor("head"),X)},killLine:function(n){return ti(n,(function(t){if(t.empty()){var e=Gn(n.doc,t.head.line).text.length;return t.head.ch==e&&t.head.line0)r=new rt(r.line,r.ch+1),n.replaceRange(o.charAt(r.ch-1)+o.charAt(r.ch-2),rt(r.line,r.ch-2),r,"+transpose");else if(r.line>n.doc.first){var i=Gn(n.doc,r.line-1).text;i&&(r=new rt(r.line,1),n.replaceRange(o.charAt(0)+n.doc.lineSeparator()+i.charAt(i.length-1),rt(r.line-1,i.length-1),r,"+transpose"))}e.push(new Cr(r,r))}n.setSelections(e)}))},newlineAndIndent:function(n){return er(n,(function(){for(var t=n.listSelections(),e=t.length-1;e>=0;e--)n.replaceRange(n.doc.lineSeparator(),t[e].anchor,t[e].head,"+input");t=n.listSelections();for(var a=0;a-1&&(ot((r=c.ranges[r]).from(),t)<0||t.xRel>0)&&(ot(r.to(),t)>0||t.xRel<0)?function(n,t,e,a){var r=n.display,o=!1,c=ar(n,(function(t){s&&(r.scroller.draggable=!1),n.state.draggingText=!1,n.state.delayingBlurEvent&&(n.hasFocus()?n.state.delayingBlurEvent=!1:Ea(n)),gn(r.wrapper.ownerDocument,"mouseup",c),gn(r.wrapper.ownerDocument,"mousemove",d),gn(r.scroller,"dragstart",p),gn(r.scroller,"drop",c),o||(zn(t),a.addNew||Jr(n.doc,e,null,null,a.extend),s&&!u||i&&9==l?setTimeout((function(){r.wrapper.ownerDocument.body.focus({preventScroll:!0}),r.input.focus()}),20):r.input.focus())})),d=function(n){o=o||Math.abs(t.clientX-n.clientX)+Math.abs(t.clientY-n.clientY)>=10},p=function(){return o=!0};s&&(r.scroller.draggable=!0),n.state.draggingText=c,c.copy=!a.moveOnDrag,mn(r.wrapper.ownerDocument,"mouseup",c),mn(r.wrapper.ownerDocument,"mousemove",d),mn(r.scroller,"dragstart",p),mn(r.scroller,"drop",c),n.state.delayingBlurEvent=!0,setTimeout((function(){return r.input.focus()}),20),r.scroller.dragDrop&&r.scroller.dragDrop()}(n,a,t,o):function(n,t,e,a){i&&Ea(n);var r=n.display,o=n.doc;zn(t);var l,s,c=o.sel,d=c.ranges;if(a.addNew&&!a.extend?(s=o.sel.contains(e),l=s>-1?d[s]:new Cr(e,e)):(l=o.sel.primary(),s=o.sel.primIndex),"rectangle"==a.unit)a.addNew||(l=new Cr(e,e)),e=ba(n,t,!0,!0),s=-1;else{var p=vi(n,e,a.unit);l=a.extend?$r(l,p.anchor,p.head,a.extend):p}a.addNew?-1==s?(s=d.length,ao(o,Sr(n,d.concat([l]),s),{scroll:!1,origin:"*mouse"})):d.length>1&&d[s].empty()&&"char"==a.unit&&!a.extend?(ao(o,Sr(n,d.slice(0,s).concat(d.slice(s+1)),0),{scroll:!1,origin:"*mouse"}),c=o.sel):no(o,s,l,q):(s=0,ao(o,new Er([l],0),q),c=o.sel);var b=e;function u(t){if(0!=ot(b,t))if(b=t,"rectangle"==a.unit){for(var r=[],i=n.options.tabSize,d=Y(Gn(o,e.line).text,e.ch,i),p=Y(Gn(o,t.line).text,t.ch,i),u=Math.min(d,p),f=Math.max(d,p),m=Math.min(e.line,t.line),h=Math.min(n.lastLine(),Math.max(e.line,t.line));m<=h;m++){var g=Gn(o,m).text,x=K(g,u,i);u==f?r.push(new Cr(rt(m,x),rt(m,x))):g.length>x&&r.push(new Cr(rt(m,x),rt(m,K(g,f,i))))}r.length||r.push(new Cr(e,e)),ao(o,Sr(n,c.ranges.slice(0,s).concat(r),s),{origin:"*mouse",scroll:!1}),n.scrollIntoView(t)}else{var w,k=l,v=vi(n,t,a.unit),y=k.anchor;ot(v.anchor,y)>0?(w=v.head,y=ct(k.from(),v.anchor)):(w=v.anchor,y=st(k.to(),v.head));var z=c.ranges.slice(0);z[s]=function(n,t){var e=t.anchor,a=t.head,r=Gn(n.doc,e.line);if(0==ot(e,a)&&e.sticky==a.sticky)return t;var o=un(r);if(!o)return t;var i=pn(o,e.ch,e.sticky),l=o[i];if(l.from!=e.ch&&l.to!=e.ch)return t;var s,c=i+(l.from==e.ch==(1!=l.level)?0:1);if(0==c||c==o.length)return t;if(a.line!=e.line)s=(a.line-e.line)*("ltr"==n.doc.direction?1:-1)>0;else{var d=pn(o,a.ch,a.sticky),p=d-i||(a.ch-e.ch)*(1==l.level?-1:1);s=d==c-1||d==c?p<0:p>0}var b=o[c+(s?-1:0)],u=s==(1==b.level),f=u?b.from:b.to,m=u?"after":"before";return e.ch==f&&e.sticky==m?t:new Cr(new rt(e.line,f,m),a)}(n,new Cr(pt(o,y),w)),ao(o,Sr(n,z,s),q)}}var f=r.wrapper.getBoundingClientRect(),m=0;function h(t){n.state.selectingText=!1,m=1/0,t&&(zn(t),r.input.focus()),gn(r.wrapper.ownerDocument,"mousemove",g),gn(r.wrapper.ownerDocument,"mouseup",x),o.history.lastSelOrigin=null}var g=ar(n,(function(t){0!==t.buttons&&Sn(t)?function t(e){var i=++m,l=ba(n,e,!0,"rectangle"==a.unit);if(l)if(0!=ot(l,b)){n.curOp.focus=P(L(n)),u(l);var s=Ia(r,o);(l.line>=s.to||l.linef.bottom?20:0;c&&setTimeout(ar(n,(function(){m==i&&(r.scroller.scrollTop+=c,t(e))})),50)}}(t):h(t)})),x=ar(n,h);n.state.selectingText=x,mn(r.wrapper.ownerDocument,"mousemove",g),mn(r.wrapper.ownerDocument,"mouseup",x)}(n,a,t,o)}(t,a,o,n):Cn(n)==e.scroller&&zn(n):2==r?(a&&Jr(t.doc,a),setTimeout((function(){return e.input.focus()}),20)):3==r&&(_?t.display.input.onContextMenu(n):Ea(t)))}}function vi(n,t,e){if("char"==e)return new Cr(t,t);if("word"==e)return n.findWordAt(t);if("line"==e)return new Cr(rt(t.line,0),pt(n.doc,rt(t.line+1,0)));var a=e(n,t);return new Cr(a.from,a.to)}function yi(n,t,e,a){var r,o;if(t.touches)r=t.touches[0].clientX,o=t.touches[0].clientY;else try{r=t.clientX,o=t.clientY}catch(n){return!1}if(r>=Math.floor(n.display.gutters.getBoundingClientRect().right))return!1;a&&zn(t);var i=n.display,l=i.lineDiv.getBoundingClientRect();if(o>l.bottom||!vn(n,e))return Fn(t);o-=l.top-i.viewOffset;for(var s=0;s=r)return xn(n,e,n,tt(n.doc,o),n.display.gutterSpecs[s].className,t),Fn(t)}}function zi(n,t){return yi(n,t,"gutterClick",!0)}function _i(n,t){Fe(n.display,t)||function(n,t){return!!vn(n,"gutterContextMenu")&&yi(n,t,"gutterContextMenu",!1)}(n,t)||wn(n,t,"contextmenu")||_||n.display.input.onContextMenu(t)}function Fi(n){n.display.wrapper.className=n.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+n.options.theme.replace(/(^|\s)\s*/g," cm-s-"),He(n)}wi.prototype.compare=function(n,t,e){return this.time+400>n&&0==ot(t,this.pos)&&e==this.button};var Ei={toString:function(){return"CodeMirror.Init"}},Ci={},Si={};function Oi(n,t,e){if(!t!=!(e&&e!=Ei)){var a=n.display.dragFunctions,r=t?mn:gn;r(n.display.scroller,"dragstart",a.start),r(n.display.scroller,"dragenter",a.enter),r(n.display.scroller,"dragover",a.over),r(n.display.scroller,"dragleave",a.leave),r(n.display.scroller,"drop",a.drop)}}function Di(n){n.options.lineWrapping?(j(n.display.wrapper,"CodeMirror-wrap"),n.display.sizer.style.minWidth="",n.display.sizerWidth=null):(C(n.display.wrapper,"CodeMirror-wrap"),Vt(n)),pa(n),fa(n),He(n),setTimeout((function(){return Wa(n)}),100)}function Ii(n,t){var e=this;if(!(this instanceof Ii))return new Ii(n,t);this.options=t=t?R(t):{},R(Ci,t,!1);var a=t.value;"string"==typeof a?a=new To(a,t.mode,null,t.lineSeparator,t.direction):t.mode&&(a.modeOption=t.mode),this.doc=a;var r=new Ii.inputStyles[t.inputStyle](this),o=this.display=new kr(n,a,r,t);for(var c in o.wrapper.CodeMirror=this,Fi(this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),Za(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new U,keySeq:null,specialChars:null},t.autofocus&&!x&&o.input.focus(),i&&l<11&&setTimeout((function(){return e.display.input.reset(!0)}),20),function(n){var t=n.display;mn(t.scroller,"mousedown",ar(n,ki)),mn(t.scroller,"dblclick",i&&l<11?ar(n,(function(t){if(!wn(n,t)){var e=ba(n,t);if(e&&!zi(n,t)&&!Fe(n.display,t)){zn(t);var a=n.findWordAt(e);Jr(n.doc,a.anchor,a.head)}}})):function(t){return wn(n,t)||zn(t)}),mn(t.scroller,"contextmenu",(function(t){return _i(n,t)})),mn(t.input.getField(),"contextmenu",(function(e){t.scroller.contains(e.target)||_i(n,e)}));var e,a={end:0};function r(){t.activeTouch&&(e=setTimeout((function(){return t.activeTouch=null}),1e3),(a=t.activeTouch).end=+new Date)}function o(n,t){if(null==t.left)return!0;var e=t.left-n.left,a=t.top-n.top;return e*e+a*a>400}mn(t.scroller,"touchstart",(function(r){if(!wn(n,r)&&!function(n){if(1!=n.touches.length)return!1;var t=n.touches[0];return t.radiusX<=1&&t.radiusY<=1}(r)&&!zi(n,r)){t.input.ensurePolled(),clearTimeout(e);var o=+new Date;t.activeTouch={start:o,moved:!1,prev:o-a.end<=300?a:null},1==r.touches.length&&(t.activeTouch.left=r.touches[0].pageX,t.activeTouch.top=r.touches[0].pageY)}})),mn(t.scroller,"touchmove",(function(){t.activeTouch&&(t.activeTouch.moved=!0)})),mn(t.scroller,"touchend",(function(e){var a=t.activeTouch;if(a&&!Fe(t,e)&&null!=a.left&&!a.moved&&new Date-a.start<300){var i,l=n.coordsChar(t.activeTouch,"page");i=!a.prev||o(a,a.prev)?new Cr(l,l):!a.prev.prev||o(a,a.prev.prev)?n.findWordAt(l):new Cr(rt(l.line,0),pt(n.doc,rt(l.line+1,0))),n.setSelection(i.anchor,i.head),n.focus(),zn(e)}r()})),mn(t.scroller,"touchcancel",r),mn(t.scroller,"scroll",(function(){t.scroller.clientHeight&&(Aa(n,t.scroller.scrollTop),Ra(n,t.scroller.scrollLeft,!0),xn(n,"scroll",n))})),mn(t.scroller,"mousewheel",(function(t){return Fr(n,t)})),mn(t.scroller,"DOMMouseScroll",(function(t){return Fr(n,t)})),mn(t.wrapper,"scroll",(function(){return t.wrapper.scrollTop=t.wrapper.scrollLeft=0})),t.dragFunctions={enter:function(t){wn(n,t)||En(t)},over:function(t){wn(n,t)||(function(n,t){var e=ba(n,t);if(e){var a=document.createDocumentFragment();va(n,e,a),n.display.dragCursor||(n.display.dragCursor=D("div",null,"CodeMirror-cursors CodeMirror-dragcursors"),n.display.lineSpace.insertBefore(n.display.dragCursor,n.display.cursorDiv)),O(n.display.dragCursor,a)}}(n,t),En(t))},start:function(t){return function(n,t){if(i&&(!n.state.draggingText||+new Date-Bo<100))En(t);else if(!wn(n,t)&&!Fe(n.display,t)&&(t.dataTransfer.setData("Text",n.getSelection()),t.dataTransfer.effectAllowed="copyMove",t.dataTransfer.setDragImage&&!u)){var e=D("img",null,null,"position: fixed; left: 0; top: 0;");e.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",b&&(e.width=e.height=1,n.display.wrapper.appendChild(e),e._top=e.offsetTop),t.dataTransfer.setDragImage(e,0,0),b&&e.parentNode.removeChild(e)}}(n,t)},drop:ar(n,Lo),leave:function(t){wn(n,t)||Ao(n)}};var s=t.input.getField();mn(s,"keyup",(function(t){return mi.call(n,t)})),mn(s,"keydown",ar(n,fi)),mn(s,"keypress",ar(n,hi)),mn(s,"focus",(function(t){return Ca(n,t)})),mn(s,"blur",(function(t){return Sa(n,t)}))}(this),Yo(),Va(this),this.curOp.forceUpdate=!0,Nr(this,a),t.autofocus&&!x||this.hasFocus()?setTimeout((function(){e.hasFocus()&&!e.state.focused&&Ca(e)}),20):Sa(this),Si)Si.hasOwnProperty(c)&&Si[c](this,t[c],Ei);hr(this),t.finishInit&&t.finishInit(this);for(var d=0;d150)){if(!a)return;e="prev"}}else c=0,e="not";"prev"==e?c=t>o.first?Y(Gn(o,t-1).text,null,i):0:"add"==e?c=s+n.options.indentUnit:"subtract"==e?c=s-n.options.indentUnit:"number"==typeof e&&(c=s+e),c=Math.max(0,c);var p="",b=0;if(n.options.indentWithTabs)for(var u=Math.floor(c/i);u;--u)b+=i,p+="\t";if(bi,s=Tn(t),c=null;if(l&&a.ranges.length>1)if(ji&&ji.text.join("\n")==t){if(a.ranges.length%ji.text.length==0){c=[];for(var d=0;d=0;b--){var u=a.ranges[b],f=u.from(),m=u.to();u.empty()&&(e&&e>0?f=rt(f.line,f.ch-e):n.state.overwrite&&!l?m=rt(m.line,Math.min(Gn(o,m.line).text.length,m.ch+$(s).length)):l&&ji&&ji.lineWise&&ji.text.join("\n")==s.join("\n")&&(f=m=rt(f.line,0)));var h={from:f,to:m,text:c?c[b%c.length]:s,origin:r||(l?"paste":n.state.cutIncoming>i?"cut":"+input")};fo(n.doc,h),pe(n,"inputRead",n,h)}t&&!l&&Ai(n,t),ja(n),n.curOp.updateInput<2&&(n.curOp.updateInput=p),n.curOp.typing=!0,n.state.pasteIncoming=n.state.cutIncoming=-1}function Li(n,t){var e=n.clipboardData&&n.clipboardData.getData("Text");if(e)return n.preventDefault(),t.isReadOnly()||t.options.disableInput||!t.hasFocus()||er(t,(function(){return Bi(t,e,0,null,"paste")})),!0}function Ai(n,t){if(n.options.electricChars&&n.options.smartIndent)for(var e=n.doc.sel,a=e.ranges.length-1;a>=0;a--){var r=e.ranges[a];if(!(r.head.ch>100||a&&e.ranges[a-1].head.line==r.head.line)){var o=n.getModeAt(r.head),i=!1;if(o.electricChars){for(var l=0;l-1){i=Pi(n,r.head.line,"smart");break}}else o.electricInput&&o.electricInput.test(Gn(n.doc,r.head.line).text.slice(0,r.head.ch))&&(i=Pi(n,r.head.line,"smart"));i&&pe(n,"electricInput",n,r.head.line)}}}function Ni(n){for(var t=[],e=[],a=0;a0?0:-1));if(isNaN(d))i=null;else{var p=e>0?d>=55296&&d<56320:d>=56320&&d<57343;i=new rt(t.line,Math.max(0,Math.min(l.text.length,t.ch+e*(p?2:1))),-e)}}else i=r?function(n,t,e,a){var r=un(t,n.doc.direction);if(!r)return ai(t,e,a);e.ch>=t.text.length?(e.ch=t.text.length,e.sticky="before"):e.ch<=0&&(e.ch=0,e.sticky="after");var o=pn(r,e.ch,e.sticky),i=r[o];if("ltr"==n.doc.direction&&i.level%2==0&&(a>0?i.to>e.ch:i.from=i.from&&b>=d.begin)){var u=p?"before":"after";return new rt(e.line,b,u)}}var f=function(n,t,a){for(var o=function(n,t){return t?new rt(e.line,s(n,1),"before"):new rt(e.line,n,"after")};n>=0&&n0==(1!=i.level),c=l?a.begin:s(a.end,-1);if(i.from<=c&&c0?d.end:s(d.begin,-1);return null==h||a>0&&h==t.text.length||!(m=f(a>0?0:r.length-1,a,c(h)))?null:m}(n.cm,l,t,e):ai(l,t,e);if(null==i){if(o||(c=t.line+s)=n.first+n.size||(t=new rt(c,t.ch,t.sticky),!(l=Gn(n,c))))return!1;t=ri(r,n.cm,l,t.line,s)}else t=i;return!0}if("char"==a||"codepoint"==a)c();else if("column"==a)c(!0);else if("word"==a||"group"==a)for(var d=null,p="group"==a,b=n.cm&&n.cm.getHelper(t,"wordChars"),u=!0;!(e<0)||c(!u);u=!1){var f=l.text.charAt(t.ch)||"\n",m=an(f,b)?"w":p&&"\n"==f?"n":!p||/\s/.test(f)?null:"p";if(!p||u||m||(m="s"),d&&d!=m){e<0&&(e=1,c(),t.sticky="after");break}if(m&&(d=m),e>0&&!c(!u))break}var h=co(n,t,o,i,!0);return it(o,h)&&(h.hitSide=!0),h}function Hi(n,t,e,a){var r,o,i=n.doc,l=t.left;if("page"==a){var s=Math.min(n.display.wrapper.clientHeight,A(n).innerHeight||i(n).documentElement.clientHeight),c=Math.max(s-.5*ia(n.display),3);r=(e>0?t.bottom:t.top)+e*c}else"line"==a&&(r=e>0?t.bottom+3:t.top-3);for(;(o=Qe(n,l,r)).outside;){if(e<0?r<=0:r>=i.height){o.hitSide=!0;break}r+=5*e}return o}var Wi=function(n){this.cm=n,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new U,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null};function Xi(n,t){var e=je(n,t.line);if(!e||e.hidden)return null;var a=Gn(n.doc,t.line),r=Me(e,a,t.line),o=un(a,n.doc.direction),i="left";o&&(i=pn(o,t.ch)%2?"right":"left");var l=Ne(r.map,t.ch,i);return l.offset="right"==l.collapse?l.end:l.start,l}function qi(n,t){return t&&(n.bad=!0),n}function Zi(n,t,e){var a;if(t==n.display.lineDiv){if(!(a=n.display.lineDiv.childNodes[e]))return qi(n.clipPos(rt(n.display.viewTo-1)),!0);t=null,e=0}else for(a=t;;a=a.parentNode){if(!a||a==n.display.lineDiv)return null;if(a.parentNode&&a.parentNode==n.display.lineDiv)break}for(var r=0;r=t.display.viewTo||o.line=t.display.viewFrom&&Xi(t,r)||{node:s[0].measure.map[2],offset:0},d=o.linea.firstLine()&&(i=rt(i.line-1,Gn(a.doc,i.line-1).length)),l.ch==Gn(a.doc,l.line).text.length&&l.liner.viewTo-1)return!1;i.line==r.viewFrom||0==(n=ua(a,i.line))?(t=nt(r.view[0].line),e=r.view[0].node):(t=nt(r.view[n].line),e=r.view[n-1].node.nextSibling);var s,c,d=ua(a,l.line);if(d==r.view.length-1?(s=r.viewTo-1,c=r.lineDiv.lastChild):(s=nt(r.view[d+1].line)-1,c=r.view[d+1].node.previousSibling),!e)return!1;for(var p=a.doc.splitLines(function(n,t,e,a,r){var o="",i=!1,l=n.doc.lineSeparator(),s=!1;function c(){i&&(o+=l,s&&(o+=l),i=s=!1)}function d(n){n&&(c(),o+=n)}function p(t){if(1==t.nodeType){var e=t.getAttribute("cm-text");if(e)return void d(e);var o,b=t.getAttribute("cm-marker");if(b){var u=n.findMarks(rt(a,0),rt(r+1,0),(h=+b,function(n){return n.id==h}));return void(u.length&&(o=u[0].find(0))&&d($n(n.doc,o.from,o.to).join(l)))}if("false"==t.getAttribute("contenteditable"))return;var f=/^(pre|div|p|li|table|br)$/i.test(t.nodeName);if(!/^br$/i.test(t.nodeName)&&0==t.textContent.length)return;f&&c();for(var m=0;m1&&b.length>1;)if($(p)==$(b))p.pop(),b.pop(),s--;else{if(p[0]!=b[0])break;p.shift(),b.shift(),t++}for(var u=0,f=0,m=p[0],h=b[0],g=Math.min(m.length,h.length);ui.ch&&x.charCodeAt(x.length-f-1)==w.charCodeAt(w.length-f-1);)u--,f++;p[p.length-1]=x.slice(0,x.length-f).replace(/^\u200b+/,""),p[0]=p[0].slice(u).replace(/\u200b+$/,"");var v=rt(t,u),y=rt(s,b.length?$(b).length-f:0);return p.length>1||p[0]||ot(v,y)?(wo(a.doc,p,v,y,"+input"),!0):void 0},Wi.prototype.ensurePolled=function(){this.forceCompositionEnd()},Wi.prototype.reset=function(){this.forceCompositionEnd()},Wi.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},Wi.prototype.readFromDOMSoon=function(){var n=this;null==this.readDOMTimeout&&(this.readDOMTimeout=setTimeout((function(){if(n.readDOMTimeout=null,n.composing){if(!n.composing.done)return;n.composing=null}n.updateFromDOM()}),80))},Wi.prototype.updateFromDOM=function(){var n=this;!this.cm.isReadOnly()&&this.pollContent()||er(this.cm,(function(){return fa(n.cm)}))},Wi.prototype.setUneditable=function(n){n.contentEditable="false"},Wi.prototype.onKeyPress=function(n){0==n.charCode||this.composing||(n.preventDefault(),this.cm.isReadOnly()||ar(this.cm,Bi)(this.cm,String.fromCharCode(null==n.charCode?n.keyCode:n.charCode),0))},Wi.prototype.readOnlyChanged=function(n){this.div.contentEditable=String("nocursor"!=n)},Wi.prototype.onContextMenu=function(){},Wi.prototype.resetPosition=function(){},Wi.prototype.needsContentAttribute=!0;var Vi=function(n){this.cm=n,this.prevInput="",this.pollingFast=!1,this.polling=new U,this.hasSelection=!1,this.composing=null,this.resetting=!1};Vi.prototype.init=function(n){var t=this,e=this,a=this.cm;this.createField(n);var r=this.textarea;function o(n){if(!wn(a,n)){if(a.somethingSelected())Ti({lineWise:!1,text:a.getSelections()});else{if(!a.options.lineWiseCopyCut)return;var t=Ni(a);Ti({lineWise:!0,text:t.text}),"cut"==n.type?a.setSelections(t.ranges,null,X):(e.prevInput="",r.value=t.text.join("\n"),B(r))}"cut"==n.type&&(a.state.cutIncoming=+new Date)}}n.wrapper.insertBefore(this.wrapper,n.wrapper.firstChild),h&&(r.style.width="0px"),mn(r,"input",(function(){i&&l>=9&&t.hasSelection&&(t.hasSelection=null),e.poll()})),mn(r,"paste",(function(n){wn(a,n)||Li(n,a)||(a.state.pasteIncoming=+new Date,e.fastPoll())})),mn(r,"cut",o),mn(r,"copy",o),mn(n.scroller,"paste",(function(t){if(!Fe(n,t)&&!wn(a,t)){if(!r.dispatchEvent)return a.state.pasteIncoming=+new Date,void e.focus();var o=new Event("paste");o.clipboardData=t.clipboardData,r.dispatchEvent(o)}})),mn(n.lineSpace,"selectstart",(function(t){Fe(n,t)||zn(t)})),mn(r,"compositionstart",(function(){var n=a.getCursor("from");e.composing&&e.composing.range.clear(),e.composing={start:n,range:a.markText(n,a.getCursor("to"),{className:"CodeMirror-composing"})}})),mn(r,"compositionend",(function(){e.composing&&(e.poll(),e.composing.range.clear(),e.composing=null)}))},Vi.prototype.createField=function(n){this.wrapper=Yi(),this.textarea=this.wrapper.firstChild},Vi.prototype.screenReaderLabelChanged=function(n){n?this.textarea.setAttribute("aria-label",n):this.textarea.removeAttribute("aria-label")},Vi.prototype.prepareSelection=function(){var n=this.cm,t=n.display,e=n.doc,a=ka(n);if(n.options.moveInputWithCursor){var r=Ge(n,e.sel.primary().head,"div"),o=t.wrapper.getBoundingClientRect(),i=t.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,r.top+i.top-o.top)),a.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,r.left+i.left-o.left))}return a},Vi.prototype.showSelection=function(n){var t=this.cm.display;O(t.cursorDiv,n.cursors),O(t.selectionDiv,n.selection),null!=n.teTop&&(this.wrapper.style.top=n.teTop+"px",this.wrapper.style.left=n.teLeft+"px")},Vi.prototype.reset=function(n){if(!(this.contextMenuPending||this.composing&&n)){var t=this.cm;if(this.resetting=!0,t.somethingSelected()){this.prevInput="";var e=t.getSelection();this.textarea.value=e,t.state.focused&&B(this.textarea),i&&l>=9&&(this.hasSelection=e)}else n||(this.prevInput=this.textarea.value="",i&&l>=9&&(this.hasSelection=null));this.resetting=!1}},Vi.prototype.getField=function(){return this.textarea},Vi.prototype.supportsTouch=function(){return!1},Vi.prototype.focus=function(){if("nocursor"!=this.cm.options.readOnly&&(!x||P(this.textarea.ownerDocument)!=this.textarea))try{this.textarea.focus()}catch(n){}},Vi.prototype.blur=function(){this.textarea.blur()},Vi.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},Vi.prototype.receivedFocus=function(){this.slowPoll()},Vi.prototype.slowPoll=function(){var n=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,(function(){n.poll(),n.cm.state.focused&&n.slowPoll()}))},Vi.prototype.fastPoll=function(){var n=!1,t=this;t.pollingFast=!0,t.polling.set(20,(function e(){t.poll()||n?(t.pollingFast=!1,t.slowPoll()):(n=!0,t.polling.set(60,e))}))},Vi.prototype.poll=function(){var n=this,t=this.cm,e=this.textarea,a=this.prevInput;if(this.contextMenuPending||this.resetting||!t.state.focused||Bn(e)&&!a&&!this.composing||t.isReadOnly()||t.options.disableInput||t.state.keySeq)return!1;var r=e.value;if(r==a&&!t.somethingSelected())return!1;if(i&&l>=9&&this.hasSelection===r||w&&/[\uf700-\uf7ff]/.test(r))return t.display.input.reset(),!1;if(t.doc.sel==t.display.selForContextMenu){var o=r.charCodeAt(0);if(8203!=o||a||(a="​"),8666==o)return this.reset(),this.cm.execCommand("undo")}for(var s=0,c=Math.min(a.length,r.length);s1e3||r.indexOf("\n")>-1?e.value=n.prevInput="":n.prevInput=r,n.composing&&(n.composing.range.clear(),n.composing.range=t.markText(n.composing.start,t.getCursor("to"),{className:"CodeMirror-composing"}))})),!0},Vi.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},Vi.prototype.onKeyPress=function(){i&&l>=9&&(this.hasSelection=null),this.fastPoll()},Vi.prototype.onContextMenu=function(n){var t=this,e=t.cm,a=e.display,r=t.textarea;t.contextMenuPending&&t.contextMenuPending();var o=ba(e,n),c=a.scroller.scrollTop;if(o&&!b){e.options.resetSelectionOnContextMenu&&-1==e.doc.sel.contains(o)&&ar(e,ao)(e.doc,Or(o),X);var d,p=r.style.cssText,u=t.wrapper.style.cssText,f=t.wrapper.offsetParent.getBoundingClientRect();if(t.wrapper.style.cssText="position: static",r.style.cssText="position: absolute; width: 30px; height: 30px;\n top: "+(n.clientY-f.top-5)+"px; left: "+(n.clientX-f.left-5)+"px;\n z-index: 1000; background: "+(i?"rgba(255, 255, 255, .05)":"transparent")+";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",s&&(d=r.ownerDocument.defaultView.scrollY),a.input.focus(),s&&r.ownerDocument.defaultView.scrollTo(null,d),a.input.reset(),e.somethingSelected()||(r.value=t.prevInput=" "),t.contextMenuPending=g,a.selForContextMenu=e.doc.sel,clearTimeout(a.detectingSelectAll),i&&l>=9&&h(),_){En(n);var m=function(){gn(window,"mouseup",m),setTimeout(g,20)};mn(window,"mouseup",m)}else setTimeout(g,50)}function h(){if(null!=r.selectionStart){var n=e.somethingSelected(),o="​"+(n?r.value:"");r.value="⇚",r.value=o,t.prevInput=n?"":"​",r.selectionStart=1,r.selectionEnd=o.length,a.selForContextMenu=e.doc.sel}}function g(){if(t.contextMenuPending==g&&(t.contextMenuPending=!1,t.wrapper.style.cssText=u,r.style.cssText=p,i&&l<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=c),null!=r.selectionStart)){(!i||i&&l<9)&&h();var n=0,o=function(){a.selForContextMenu==e.doc.sel&&0==r.selectionStart&&r.selectionEnd>0&&"​"==t.prevInput?ar(e,bo)(e):n++<10?a.detectingSelectAll=setTimeout(o,500):(a.selForContextMenu=null,a.input.reset())};a.detectingSelectAll=setTimeout(o,200)}}},Vi.prototype.readOnlyChanged=function(n){n||this.reset(),this.textarea.disabled="nocursor"==n,this.textarea.readOnly=!!n},Vi.prototype.setUneditable=function(){},Vi.prototype.needsContentAttribute=!1,function(n){var t=n.optionHandlers;function e(e,a,r,o){n.defaults[e]=a,r&&(t[e]=o?function(n,t,e){e!=Ei&&r(n,t,e)}:r)}n.defineOption=e,n.Init=Ei,e("value","",(function(n,t){return n.setValue(t)}),!0),e("mode",null,(function(n,t){n.doc.modeOption=t,jr(n)}),!0),e("indentUnit",2,jr,!0),e("indentWithTabs",!1),e("smartIndent",!0),e("tabSize",4,(function(n){Tr(n),He(n),fa(n)}),!0),e("lineSeparator",null,(function(n,t){if(n.doc.lineSep=t,t){var e=[],a=n.doc.first;n.doc.iter((function(n){for(var r=0;;){var o=n.text.indexOf(t,r);if(-1==o)break;r=o+t.length,e.push(rt(a,o))}a++}));for(var r=e.length-1;r>=0;r--)wo(n.doc,t,e[r],rt(e[r].line,e[r].ch+t.length))}})),e("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/g,(function(n,t,e){n.state.specialChars=new RegExp(t.source+(t.test("\t")?"":"|\t"),"g"),e!=Ei&&n.refresh()})),e("specialCharPlaceholder",ee,(function(n){return n.refresh()}),!0),e("electricChars",!0),e("inputStyle",x?"contenteditable":"textarea",(function(){throw new Error("inputStyle can not (yet) be changed in a running editor")}),!0),e("spellcheck",!1,(function(n,t){return n.getInputField().spellcheck=t}),!0),e("autocorrect",!1,(function(n,t){return n.getInputField().autocorrect=t}),!0),e("autocapitalize",!1,(function(n,t){return n.getInputField().autocapitalize=t}),!0),e("rtlMoveVisually",!v),e("wholeLineUpdateBefore",!0),e("theme","default",(function(n){Fi(n),wr(n)}),!0),e("keyMap","default",(function(n,t,e){var a=ni(t),r=e!=Ei&&ni(e);r&&r.detach&&r.detach(n,a),a.attach&&a.attach(n,r||null)})),e("extraKeys",null),e("configureMouse",null),e("lineWrapping",!1,Di,!0),e("gutters",[],(function(n,t){n.display.gutterSpecs=gr(t,n.options.lineNumbers),wr(n)}),!0),e("fixedGutter",!0,(function(n,t){n.display.gutters.style.left=t?ca(n.display)+"px":"0",n.refresh()}),!0),e("coverGutterNextToScrollbar",!1,(function(n){return Wa(n)}),!0),e("scrollbarStyle","native",(function(n){Za(n),Wa(n),n.display.scrollbars.setScrollTop(n.doc.scrollTop),n.display.scrollbars.setScrollLeft(n.doc.scrollLeft)}),!0),e("lineNumbers",!1,(function(n,t){n.display.gutterSpecs=gr(n.options.gutters,t),wr(n)}),!0),e("firstLineNumber",1,wr,!0),e("lineNumberFormatter",(function(n){return n}),wr,!0),e("showCursorWhenSelecting",!1,wa,!0),e("resetSelectionOnContextMenu",!0),e("lineWiseCopyCut",!0),e("pasteLinesPerSelection",!0),e("selectionsMayTouch",!1),e("readOnly",!1,(function(n,t){"nocursor"==t&&(Sa(n),n.display.input.blur()),n.display.input.readOnlyChanged(t)})),e("screenReaderLabel",null,(function(n,t){t=""===t?null:t,n.display.input.screenReaderLabelChanged(t)})),e("disableInput",!1,(function(n,t){t||n.display.input.reset()}),!0),e("dragDrop",!0,Oi),e("allowDropFileTypes",null),e("cursorBlinkRate",530),e("cursorScrollMargin",0),e("cursorHeight",1,wa,!0),e("singleCursorHeightPerLine",!0,wa,!0),e("workTime",100),e("workDelay",100),e("flattenSpans",!0,Tr,!0),e("addModeClass",!1,Tr,!0),e("pollInterval",100),e("undoDepth",200,(function(n,t){return n.doc.history.undoDepth=t})),e("historyEventDelay",1250),e("viewportMargin",10,(function(n){return n.refresh()}),!0),e("maxHighlightLength",1e4,Tr,!0),e("moveInputWithCursor",!0,(function(n,t){t||n.display.input.resetPosition()})),e("tabindex",null,(function(n,t){return n.display.input.getField().tabIndex=t||""})),e("autofocus",null),e("direction","ltr",(function(n,t){return n.doc.setDirection(t)}),!0),e("phrases",null)}(Ii),function(n){var t=n.optionHandlers,e=n.helpers={};n.prototype={constructor:n,focus:function(){A(this).focus(),this.display.input.focus()},setOption:function(n,e){var a=this.options,r=a[n];a[n]==e&&"mode"!=n||(a[n]=e,t.hasOwnProperty(n)&&ar(this,t[n])(this,e,r),xn(this,"optionChange",this,n))},getOption:function(n){return this.options[n]},getDoc:function(){return this.doc},addKeyMap:function(n,t){this.state.keyMaps[t?"push":"unshift"](ni(n))},removeKeyMap:function(n){for(var t=this.state.keyMaps,e=0;ee&&(Pi(this,r.head.line,n,!0),e=r.head.line,a==this.doc.sel.primIndex&&ja(this));else{var o=r.from(),i=r.to(),l=Math.max(e,o.line);e=Math.min(this.lastLine(),i.line-(i.ch?0:1))+1;for(var s=l;s0&&no(this.doc,a,new Cr(o,c[a].to()),X)}}})),getTokenAt:function(n,t){return yt(this,n,t)},getLineTokens:function(n,t){return yt(this,rt(n),t,!0)},getTokenTypeAt:function(n){n=pt(this.doc,n);var t,e=ht(this,Gn(this.doc,n.line)),a=0,r=(e.length-1)/2,o=n.ch;if(0==o)t=e[2];else for(;;){var i=a+r>>1;if((i?e[2*i-1]:0)>=o)r=i;else{if(!(e[2*i+1]o&&(n=o,r=!0),a=Gn(this.doc,n)}else a=n;return Ze(this,a,{top:0,left:0},t||"page",e||r).top+(r?this.doc.height-Zt(a):0)},defaultTextHeight:function(){return ia(this.display)},defaultCharWidth:function(){return la(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(n,t,e,a,r){var o,i,l,s=this.display,c=(n=Ge(this,pt(this.doc,n))).bottom,d=n.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),s.sizer.appendChild(t),"over"==a)c=n.top;else if("above"==a||"near"==a){var p=Math.max(s.wrapper.clientHeight,this.doc.height),b=Math.max(s.sizer.clientWidth,s.lineSpace.clientWidth);("above"==a||n.bottom+t.offsetHeight>p)&&n.top>t.offsetHeight?c=n.top-t.offsetHeight:n.bottom+t.offsetHeight<=p&&(c=n.bottom),d+t.offsetWidth>b&&(d=b-t.offsetWidth)}t.style.top=c+"px",t.style.left=t.style.right="","right"==r?(d=s.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==r?d=0:"middle"==r&&(d=(s.sizer.clientWidth-t.offsetWidth)/2),t.style.left=d+"px"),e&&(o=this,i={left:d,top:c,right:d+t.offsetWidth,bottom:c+t.offsetHeight},null!=(l=Ma(o,i)).scrollTop&&Aa(o,l.scrollTop),null!=l.scrollLeft&&Ra(o,l.scrollLeft))},triggerOnKeyDown:rr(fi),triggerOnKeyPress:rr(hi),triggerOnKeyUp:mi,triggerOnMouseDown:rr(ki),execCommand:function(n){if(oi.hasOwnProperty(n))return oi[n].call(null,this)},triggerElectric:rr((function(n){Ai(this,n)})),findPosH:function(n,t,e,a){var r=1;t<0&&(r=-1,t=-t);for(var o=pt(this.doc,n),i=0;i0&&i(t.charAt(e-1));)--e;for(;a.5||this.options.lineWrapping)&&pa(this),xn(this,"refresh",this)})),swapDoc:rr((function(n){var t=this.doc;return t.cm=null,this.state.selectingText&&this.state.selectingText(),Nr(this,n),He(this),this.display.input.reset(),Ta(this,n.scrollLeft,n.scrollTop),this.curOp.forceScroll=!0,pe(this,"swapDoc",this,t),t})),phrase:function(n){var t=this.options.phrases;return t&&Object.prototype.hasOwnProperty.call(t,n)?t[n]:n},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},yn(n),n.registerHelper=function(t,a,r){e.hasOwnProperty(t)||(e[t]=n[t]={_global:[]}),e[t][a]=r},n.registerGlobalHelper=function(t,a,r,o){n.registerHelper(t,a,o),e[t]._global.push({pred:r,val:o})}}(Ii);var Gi="iter insert remove copy getEditor constructor".split(" ");for(var $i in To.prototype)To.prototype.hasOwnProperty($i)&&H(Gi,$i)<0&&(Ii.prototype[$i]=function(n){return function(){return n.apply(this.doc,arguments)}}(To.prototype[$i]));return yn(To),Ii.inputStyles={textarea:Vi,contenteditable:Wi},Ii.defineMode=function(n){Ii.defaults.mode||"null"==n||(Ii.defaults.mode=n),Yn.apply(this,arguments)},Ii.defineMIME=function(n,t){Rn[n]=t},Ii.defineMode("null",(function(){return{token:function(n){return n.skipToEnd()}}})),Ii.defineMIME("text/plain","null"),Ii.defineExtension=function(n,t){Ii.prototype[n]=t},Ii.defineDocExtension=function(n,t){To.prototype[n]=t},Ii.fromTextArea=function(n,t){if((t=t?R(t):{}).value=n.value,!t.tabindex&&n.tabIndex&&(t.tabindex=n.tabIndex),!t.placeholder&&n.placeholder&&(t.placeholder=n.placeholder),null==t.autofocus){var e=P(n.ownerDocument);t.autofocus=e==n||null!=n.getAttribute("autofocus")&&e==document.body}function a(){n.value=l.getValue()}var r;if(n.form&&(mn(n.form,"submit",a),!t.leaveSubmitMethodAlone)){var o=n.form;r=o.submit;try{var i=o.submit=function(){a(),o.submit=r,o.submit(),o.submit=i}}catch(n){}}t.finishInit=function(e){e.save=a,e.getTextArea=function(){return n},e.toTextArea=function(){e.toTextArea=isNaN,a(),n.parentNode.removeChild(e.getWrapperElement()),n.style.display="",n.form&&(gn(n.form,"submit",a),t.leaveSubmitMethodAlone||"function"!=typeof n.form.submit||(n.form.submit=r))}},n.style.display="none";var l=Ii((function(t){return n.parentNode.insertBefore(t,n.nextSibling)}),t);return l},function(n){n.off=gn,n.on=mn,n.wheelEventPixels=_r,n.Doc=To,n.splitLines=Tn,n.countColumn=Y,n.findColumn=K,n.isWordChar=en,n.Pass=W,n.signal=xn,n.Line=Gt,n.changeEnd=Dr,n.scrollbarModel=qa,n.Pos=rt,n.cmpPos=ot,n.modes=Nn,n.mimeModes=Rn,n.resolveMode=Un,n.getMode=Hn,n.modeExtensions=Wn,n.extendMode=Xn,n.copyState=qn,n.startState=Kn,n.innerMode=Zn,n.commands=oi,n.keyMap=Zo,n.keyName=Qo,n.isModifierKey=$o,n.lookupKey=Go,n.normalizeKeyMap=Vo,n.StringStream=Vn,n.SharedTextMarker=Io,n.TextMarker=Oo,n.LineWidget=Eo,n.e_preventDefault=zn,n.e_stopPropagation=_n,n.e_stop=En,n.addClass=j,n.contains=M,n.rmClass=C,n.keyNames=Ho}(Ii),Ii.version="5.65.10",Ii}()},function(n,t,e){!function(n){"use strict";n.defineMode("shell",(function(){var t={};function e(n,e){for(var a=0;a1&&n.eat("$");var e=n.next();return/['"({]/.test(e)?(t.tokens[0]=l(e,"("==e?"quote":"{"==e?"def":"string"),d(n,t)):(/\d/.test(e)||n.eatWhile(/\w/),t.tokens.shift(),"def")};function d(n,t){return(t.tokens[0]||i)(n,t)}return{startState:function(){return{tokens:[]}},token:function(n,t){return d(n,t)},closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}})),n.defineMIME("text/x-sh","shell"),n.defineMIME("application/x-sh","shell")}(e(61))},function(n,t,e){var a=e(146),r=e(3);"string"==typeof a&&(a=[[n.i,a,""]]),n.exports=a.locals||{},n.exports._getContent=function(){return a},n.exports._getCss=function(){return a.toString()},n.exports._insertCss=function(n){return r(a,n)}},function(n,t,e){var a=e(176),r=e(65),o=e(66),i=e(34),l=e(67),s=e(68),c=e(22)("socket.io-client:manager"),d=e(179),p=e(180),b=Object.prototype.hasOwnProperty;function u(n,t){if(!(this instanceof u))return new u(n,t);n&&"object"==typeof n&&(t=n,n=void 0),(t=t||{}).path=t.path||"/socket.io",this.nsps={},this.subs=[],this.opts=t,this.reconnection(!1!==t.reconnection),this.reconnectionAttempts(t.reconnectionAttempts||1/0),this.reconnectionDelay(t.reconnectionDelay||1e3),this.reconnectionDelayMax(t.reconnectionDelayMax||5e3),this.randomizationFactor(t.randomizationFactor||.5),this.backoff=new p({min:this.reconnectionDelay(),max:this.reconnectionDelayMax(),jitter:this.randomizationFactor()}),this.timeout(null==t.timeout?2e4:t.timeout),this.readyState="closed",this.uri=n,this.connecting=[],this.lastPing=null,this.encoding=!1,this.packetBuffer=[],this.encoder=new i.Encoder,this.decoder=new i.Decoder,this.autoConnect=!1!==t.autoConnect,this.autoConnect&&this.open()}n.exports=u,u.prototype.emitAll=function(){for(var n in this.emit.apply(this,arguments),this.nsps)b.call(this.nsps,n)&&this.nsps[n].emit.apply(this.nsps[n],arguments)},u.prototype.updateSocketIds=function(){for(var n in this.nsps)b.call(this.nsps,n)&&(this.nsps[n].id=this.engine.id)},o(u.prototype),u.prototype.reconnection=function(n){return arguments.length?(this._reconnection=!!n,this):this._reconnection},u.prototype.reconnectionAttempts=function(n){return arguments.length?(this._reconnectionAttempts=n,this):this._reconnectionAttempts},u.prototype.reconnectionDelay=function(n){return arguments.length?(this._reconnectionDelay=n,this.backoff&&this.backoff.setMin(n),this):this._reconnectionDelay},u.prototype.randomizationFactor=function(n){return arguments.length?(this._randomizationFactor=n,this.backoff&&this.backoff.setJitter(n),this):this._randomizationFactor},u.prototype.reconnectionDelayMax=function(n){return arguments.length?(this._reconnectionDelayMax=n,this.backoff&&this.backoff.setMax(n),this):this._reconnectionDelayMax},u.prototype.timeout=function(n){return arguments.length?(this._timeout=n,this):this._timeout},u.prototype.maybeReconnectOnOpen=function(){!this.reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()},u.prototype.open=u.prototype.connect=function(n,t){if(c("readyState %s",this.readyState),~this.readyState.indexOf("open"))return this;c("opening %s",this.uri),this.engine=a(this.uri,this.opts);var e=this.engine,r=this;this.readyState="opening",this.skipReconnect=!1;var o=l(e,"open",(function(){r.onopen(),n&&n()})),i=l(e,"error",(function(t){if(c("connect_error"),r.cleanup(),r.readyState="closed",r.emitAll("connect_error",t),n){var e=new Error("Connection error");e.data=t,n(e)}else r.maybeReconnectOnOpen()}));if(!1!==this._timeout){var s=this._timeout;c("connect attempt will timeout after %d",s);var d=setTimeout((function(){c("connect attempt timed out after %d",s),o.destroy(),e.close(),e.emit("error","timeout"),r.emitAll("connect_timeout",s)}),s);this.subs.push({destroy:function(){clearTimeout(d)}})}return this.subs.push(o),this.subs.push(i),this},u.prototype.onopen=function(){c("open"),this.cleanup(),this.readyState="open",this.emit("open");var n=this.engine;this.subs.push(l(n,"data",s(this,"ondata"))),this.subs.push(l(n,"ping",s(this,"onping"))),this.subs.push(l(n,"pong",s(this,"onpong"))),this.subs.push(l(n,"error",s(this,"onerror"))),this.subs.push(l(n,"close",s(this,"onclose"))),this.subs.push(l(this.decoder,"decoded",s(this,"ondecoded")))},u.prototype.onping=function(){this.lastPing=new Date,this.emitAll("ping")},u.prototype.onpong=function(){this.emitAll("pong",new Date-this.lastPing)},u.prototype.ondata=function(n){this.decoder.add(n)},u.prototype.ondecoded=function(n){this.emit("packet",n)},u.prototype.onerror=function(n){c("error",n),this.emitAll("error",n)},u.prototype.socket=function(n,t){var e=this.nsps[n];if(!e){e=new r(this,n,t),this.nsps[n]=e;var a=this;e.on("connecting",o),e.on("connect",(function(){e.id=a.engine.id})),this.autoConnect&&o()}function o(){~d(a.connecting,e)||a.connecting.push(e)}return e},u.prototype.destroy=function(n){var t=d(this.connecting,n);~t&&this.connecting.splice(t,1),this.connecting.length||this.close()},u.prototype.packet=function(n){c("writing packet %j",n);var t=this;n.query&&0===n.type&&(n.nsp+="?"+n.query),t.encoding?t.packetBuffer.push(n):(t.encoding=!0,this.encoder.encode(n,(function(e){for(var a=0;a0&&!this.encoding){var n=this.packetBuffer.shift();this.packet(n)}},u.prototype.cleanup=function(){c("cleanup");for(var n=this.subs.length,t=0;t=this._reconnectionAttempts)c("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var t=this.backoff.duration();c("will wait %dms before reconnect attempt",t),this.reconnecting=!0;var e=setTimeout((function(){n.skipReconnect||(c("attempting reconnect"),n.emitAll("reconnect_attempt",n.backoff.attempts),n.emitAll("reconnecting",n.backoff.attempts),n.skipReconnect||n.open((function(t){t?(c("reconnect attempt error"),n.reconnecting=!1,n.reconnect(),n.emitAll("reconnect_error",t.data)):(c("reconnect success"),n.onreconnect())})))}),t);this.subs.push({destroy:function(){clearTimeout(e)}})}},u.prototype.onreconnect=function(){var n=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",n)}},function(n,t,e){var a=e(34),r=e(66),o=e(177),i=e(67),l=e(68),s=e(22)("socket.io-client:socket"),c=e(178);n.exports=b;var d={connect:1,connect_error:1,connect_timeout:1,connecting:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1,ping:1,pong:1},p=r.prototype.emit;function b(n,t,e){this.io=n,this.nsp=t,this.json=this,this.ids=0,this.acks={},this.receiveBuffer=[],this.sendBuffer=[],this.connected=!1,this.disconnected=!0,e&&e.query&&(this.query=e.query),this.io.autoConnect&&this.open()}r(b.prototype),b.prototype.subEvents=function(){if(!this.subs){var n=this.io;this.subs=[i(n,"open",l(this,"onopen")),i(n,"packet",l(this,"onpacket")),i(n,"close",l(this,"onclose"))]}},b.prototype.open=b.prototype.connect=function(){return this.connected||(this.subEvents(),this.io.open(),"open"===this.io.readyState&&this.onopen(),this.emit("connecting")),this},b.prototype.send=function(){var n=o(arguments);return n.unshift("message"),this.emit.apply(this,n),this},b.prototype.emit=function(n){if(d.hasOwnProperty(n))return p.apply(this,arguments),this;var t=o(arguments),e=a.EVENT;c(t)&&(e=a.BINARY_EVENT);var r={type:e,data:t,options:{}};return r.options.compress=!this.flags||!1!==this.flags.compress,"function"==typeof t[t.length-1]&&(s("emitting packet with ack id %d",this.ids),this.acks[this.ids]=t.pop(),r.id=this.ids++),this.connected?this.packet(r):this.sendBuffer.push(r),delete this.flags,this},b.prototype.packet=function(n){n.nsp=this.nsp,this.io.packet(n)},b.prototype.onopen=function(){s("transport is open - connecting"),"/"!==this.nsp&&(this.query?this.packet({type:a.CONNECT,query:this.query}):this.packet({type:a.CONNECT}))},b.prototype.onclose=function(n){s("close (%s)",n),this.connected=!1,this.disconnected=!0,delete this.id,this.emit("disconnect",n)},b.prototype.onpacket=function(n){if(n.nsp===this.nsp)switch(n.type){case a.CONNECT:this.onconnect();break;case a.EVENT:case a.BINARY_EVENT:this.onevent(n);break;case a.ACK:case a.BINARY_ACK:this.onack(n);break;case a.DISCONNECT:this.ondisconnect();break;case a.ERROR:this.emit("error",n.data)}},b.prototype.onevent=function(n){var t=n.data||[];s("emitting event %j",t),null!=n.id&&(s("attaching ack callback to event"),t.push(this.ack(n.id))),this.connected?p.apply(this,t):this.receiveBuffer.push(t)},b.prototype.ack=function(n){var t=this,e=!1;return function(){if(!e){e=!0;var r=o(arguments);s("sending ack %j",r);var i=c(r)?a.BINARY_ACK:a.ACK;t.packet({type:i,id:n,data:r})}}},b.prototype.onack=function(n){var t=this.acks[n.id];"function"==typeof t?(s("calling ack %s with %j",n.id,n.data),t.apply(this,n.data),delete this.acks[n.id]):s("bad ack %s",n.id)},b.prototype.onconnect=function(){this.connected=!0,this.disconnected=!1,this.emit("connect"),this.emitBuffered()},b.prototype.emitBuffered=function(){var n;for(n=0;n0&&r[r.length-1])||6!==o[0]&&2!==o[0])){i=0;continue}if(3===o[0]&&(!r||o[1]>r[0]&&o[1]0&&s.forEach((function(n){n.key&&n.value&&(p[n.key]=n.value)}));var b={mcpServers:(t={},t[r||"your-server-name"]={command:i||"node",args:l?l.split("\n").filter(Boolean):["path/to/your/server.js"],env:Object.keys(p).length>0?p:void 0},t)};return JSON.stringify(b,null,2)}return"streamable-http"===o?JSON.stringify({mcpServers:(e={},e[r||"your-server-name"]={url:c},e)},null,2):"sse"===o?JSON.stringify({mcpServers:(a={},a[r||"your-server-name"]={url:d},a)},null,2):"{}"}},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var r=Object.getOwnPropertyDescriptor(t,e);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,r)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),r=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),o=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return r(t,n),t},i=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=o(e(0)),s=i(e(8)),c=s.default.mcpInspectorWebPort,d=s.default.mcpInspectorServerPort;t.default=function(n){var t=n.config,e=(0,l.useMemo)((function(){return function(n){var t,e={MCP_PROXY_PORT:d.toString(),transport:(null==n?void 0:n.transport)||"streamable-http",serverCommand:(null==n?void 0:n.serverCommand)||"npx",serverArgs:(null===(t=null==n?void 0:n.serverArgs)||void 0===t?void 0:t.join(","))||"",serverUrl:(null==n?void 0:n.serverUrl)||"".concat(location.origin,"/mcp-endpoint/your-server-id/mcp")};return new URLSearchParams(e).toString()}(t)}),[t]);return l.default.createElement("div",{className:"mcp-inspector-iframe",style:{minHeight:800,height:800}},l.default.createElement("iframe",{src:"".concat(location.protocol,"//").concat(location.hostname,":").concat(c,"?").concat(e),style:{width:"100%",height:"100%"}}))}},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var r=Object.getOwnPropertyDescriptor(t,e);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,r)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),r=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),o=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return r(t,n),t},i=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=o(e(0)),s=(o(e(75)),e(76),e(10)),c=e(23),d=e(7),p=i(e(77)),b=i(e(273)),u=e(274),f=i(e(40));e(278),e(280);t.default=function(n,t){var e=n.state.url,a=(0,c.matchRoutes)(f.default,e).map((function(n){var t=n.route,e=t.component&&t.component.fetch;return e instanceof Function?e():Promise.resolve(null)}));return Promise.all(a).then((function(t){var a=n.state;t.forEach((function(n){Object.assign(a,n)})),n.state=Object.assign({},n.state,a);var r=(0,u.create)(a);return function(){return l.createElement(b.default,null,l.createElement("div",{style:{height:"100%"}},l.createElement(s.Provider,{store:r},l.createElement(d.StaticRouter,{location:e,context:{}},l.createElement(p.default,null)))))}}))}},function(n,t){n.exports=require("react-dom")},function(n,t){n.exports=require("react-hot-loader")},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var r=Object.getOwnPropertyDescriptor(t,e);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,r)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),r=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),o=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return r(t,n),t},i=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=o(e(0)),s=e(10),c=e(23),d=e(7),p=e(1),b=i(e(78)),u=e(24),f=i(e(40)),m=o(e(42)),h=e(266);e(267),e(269),e(271);t.default=function(){var n=(0,u.bindActionCreators)(m,(0,s.useDispatch)()).changeLocalIp;return(0,l.useEffect)((function(){var t,e,a,r;window.console.log("%cApp current version: v".concat(h.version),"font-family: Cabin, Helvetica, Arial, sans-serif;text-align: left;font-size:32px;color:#B21212;"),t=window,e=document,t.hj=t.hj||function(){(t.hj.q=t.hj.q||[]).push(arguments)},t._hjSettings={hjid:2133522,hjsv:6},a=e.getElementsByTagName("head")[0],(r=e.createElement("script")).async=1,r.src="https://static.hotjar.com/c/hotjar-"+t._hjSettings.hjid+".js?sv="+t._hjSettings.hjsv,a.appendChild(r),n()}),[]),l.default.createElement("div",{style:{height:"100%"}},l.default.createElement(p.ConfigProvider,{locale:b.default},l.default.createElement(d.Switch,null,(0,c.renderRoutes)(f.default))))}},function(n,t,e){"use strict";var a=e(16);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=a(e(79)).default;t.default=r},function(n,t,e){"use strict";var a=e(16);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r=a(e(80)),o=a(e(38)),i=a(e(39)),l=a(e(83)),s="${label}不是一个有效的${type}",c={locale:"zh-cn",Pagination:r.default,DatePicker:o.default,TimePicker:i.default,Calendar:l.default,global:{placeholder:"请选择"},Table:{filterTitle:"筛选",filterConfirm:"确定",filterReset:"重置",filterEmptyText:"无筛选项",selectAll:"全选当页",selectInvert:"反选当页",selectNone:"清空所有",selectionAll:"全选所有",sortTitle:"排序",expand:"展开行",collapse:"关闭行",triggerDesc:"点击降序",triggerAsc:"点击升序",cancelSort:"取消排序"},Modal:{okText:"确定",cancelText:"取消",justOkText:"知道了"},Popconfirm:{cancelText:"取消",okText:"确定"},Transfer:{searchPlaceholder:"请输入搜索内容",itemUnit:"项",itemsUnit:"项",remove:"删除",selectCurrent:"全选当页",removeCurrent:"删除当页",selectAll:"全选所有",removeAll:"删除全部",selectInvert:"反选当页"},Upload:{uploading:"文件上传中",removeFile:"删除文件",uploadError:"上传错误",previewFile:"预览文件",downloadFile:"下载文件"},Empty:{description:"暂无数据"},Icon:{icon:"图标"},Text:{edit:"编辑",copy:"复制",copied:"复制成功",expand:"展开"},PageHeader:{back:"返回"},Form:{optional:"(可选)",defaultValidateMessages:{default:"字段验证错误${label}",required:"请输入${label}",enum:"${label}必须是其中一个[${enum}]",whitespace:"${label}不能为空字符",date:{format:"${label}日期格式无效",parse:"${label}不能转换为日期",invalid:"${label}是一个无效日期"},types:{string:s,method:s,array:s,object:s,number:s,date:s,boolean:s,integer:s,float:s,regexp:s,email:s,url:s,hex:s},string:{len:"${label}须为${len}个字符",min:"${label}最少${min}个字符",max:"${label}最多${max}个字符",range:"${label}须在${min}-${max}字符之间"},number:{len:"${label}必须等于${len}",min:"${label}最小值为${min}",max:"${label}最大值为${max}",range:"${label}须在${min}-${max}之间"},array:{len:"须为${len}个${label}",min:"最少${min}个${label}",max:"最多${max}个${label}",range:"${label}数量须在${min}-${max}之间"},pattern:{mismatch:"${label}与模式不匹配${pattern}"}}},Image:{preview:"预览"}};t.default=c},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={items_per_page:"条/页",jump_to:"跳至",jump_to_confirm:"确定",page:"页",prev_page:"上一页",next_page:"下一页",prev_5:"向前 5 页",next_5:"向后 5 页",prev_3:"向前 3 页",next_3:"向后 3 页",page_size:"页码"}},function(n,t){function e(){return n.exports=e=Object.assign?Object.assign.bind():function(n){for(var t=1;t-1}))}));n.length&&w([n[0].path])}),[r]),l.default.createElement(g,{className:"dt-layout-header header_component"},l.default.createElement("div",{className:"dt-header-log-wrapper logo"},l.default.createElement(c.Link,{to:"/page/toolbox"},l.default.createElement("img",{className:"logo_img",src:u.default}),l.default.createElement("span",{className:"system-title"},"Doraemon"))),l.default.createElement("div",{className:"menu_content"},l.default.createElement(p.Menu,{mode:"horizontal",theme:"dark",onClick:function(n){w(n.key)},selectedKeys:i},x.map((function(n){var t=n.children,e=n.name,a=n.path,r=n.icon;return Array.isArray(t)&&t.length>0?l.default.createElement(h,{key:e,title:l.default.createElement("span",null,r,l.default.createElement("span",null,"Navigation Two"))},t.map((function(n){return l.default.createElement(p.Menu.Item,{key:n.path},l.default.createElement(c.Link,{to:n.path},n.icon,l.default.createElement("span",null,n.name)))}))):l.default.createElement(p.Menu.Item,{key:a},l.default.createElement(c.Link,{to:a},r,l.default.createElement("span",null,e)))}))),l.default.createElement("div",null,l.default.createElement("a",{href:(null===m.default||void 0===m.default?void 0:m.default.helpDocUrl)||"",rel:"noopener noreferrer",target:"_blank"},l.default.createElement(d.QuestionCircleOutlined,{className:"help-link"})),l.default.createElement("span",{className:"local-ip ml-20"},"本机IP: ".concat(a)),l.default.createElement(d.SyncOutlined,{className:"refresh-cion",onClick:function(){return k(!0)}}))))}},function(n,t,e){n.exports=e.p+"img/logo.ff9eed58.png"},function(n,t,e){var a=e(88),r=e(3);"string"==typeof a&&(a=[[n.i,a,""]]),n.exports=a.locals||{},n.exports._getContent=function(){return a},n.exports._getCss=function(){return a.toString()},n.exports._insertCss=function(n){return r(a,n)}},function(n,t,e){(t=e(2)(!1)).push([n.i,".header_component .menu_content{-webkit-box-flex:1;-webkit-flex:1;flex:1;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:0 20px}.header_component .menu_content .ant-menu{overflow:hidden;font-size:14px}.header_component .help-link{color:#fff;font-size:18px;vertical-align:sub;cursor:pointer}.header_component .refresh-cion{color:#fff;font-size:16px;margin-left:10px}",""]),n.exports=t},function(n,t,e){n.exports={default:e(90),__esModule:!0}},function(n,t,e){var a=e(11),r=a.JSON||(a.JSON={stringify:JSON.stringify});n.exports=function(n){return r.stringify.apply(r,arguments)}},function(n,t,e){"use strict";t.__esModule=!0;var a=o(e(92)),r=o(e(117));function o(n){return n&&n.__esModule?n:{default:n}}t.default=function(n,t){if(Array.isArray(n))return n;if((0,a.default)(Object(n)))return function(n,t){var e=[],a=!0,o=!1,i=void 0;try{for(var l,s=(0,r.default)(n);!(a=(l=s.next()).done)&&(e.push(l.value),!t||e.length!==t);a=!0);}catch(n){o=!0,i=n}finally{try{!a&&s.return&&s.return()}finally{if(o)throw i}}return e}(n,t);throw new TypeError("Invalid attempt to destructure non-iterable instance")}},function(n,t,e){n.exports={default:e(93),__esModule:!0}},function(n,t,e){e(44),e(55),n.exports=e(116)},function(n,t,e){"use strict";var a=e(95),r=e(96),o=e(14),i=e(25);n.exports=e(46)(Array,"Array",(function(n,t){this._t=i(n),this._i=0,this._k=t}),(function(){var n=this._t,t=this._k,e=this._i++;return!n||e>=n.length?(this._t=void 0,r(1)):r(0,"keys"==t?e:"values"==t?n[e]:[e,n[e]])}),"values"),o.Arguments=o.Array,a("keys"),a("values"),a("entries")},function(n,t){n.exports=function(){}},function(n,t){n.exports=function(n,t){return{value:t,done:!!n}}},function(n,t,e){var a=e(45);n.exports=Object("z").propertyIsEnumerable(0)?Object:function(n){return"String"==a(n)?n.split(""):Object(n)}},function(n,t,e){var a=e(12),r=e(11),o=e(99),i=e(15),l=e(19),s=function(n,t,e){var c,d,p,b=n&s.F,u=n&s.G,f=n&s.S,m=n&s.P,h=n&s.B,g=n&s.W,x=u?r:r[t]||(r[t]={}),w=x.prototype,k=u?a:f?a[t]:(a[t]||{}).prototype;for(c in u&&(e=t),e)(d=!b&&k&&void 0!==k[c])&&l(x,c)||(p=d?k[c]:e[c],x[c]=u&&"function"!=typeof k[c]?e[c]:h&&d?o(p,a):g&&k[c]==p?function(n){var t=function(t,e,a){if(this instanceof n){switch(arguments.length){case 0:return new n;case 1:return new n(t);case 2:return new n(t,e)}return new n(t,e,a)}return n.apply(this,arguments)};return t.prototype=n.prototype,t}(p):m&&"function"==typeof p?o(Function.call,p):p,m&&((x.virtual||(x.virtual={}))[c]=p,n&s.R&&w&&!w[c]&&i(w,c,p)))};s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,n.exports=s},function(n,t,e){var a=e(100);n.exports=function(n,t,e){if(a(n),void 0===t)return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,a){return n.call(t,e,a)};case 3:return function(e,a,r){return n.call(t,e,a,r)}}return function(){return n.apply(t,arguments)}}},function(n,t){n.exports=function(n){if("function"!=typeof n)throw TypeError(n+" is not a function!");return n}},function(n,t,e){n.exports=!e(18)&&!e(48)((function(){return 7!=Object.defineProperty(e(49)("div"),"a",{get:function(){return 7}}).a}))},function(n,t,e){var a=e(28);n.exports=function(n,t){if(!a(n))return n;var e,r;if(t&&"function"==typeof(e=n.toString)&&!a(r=e.call(n)))return r;if("function"==typeof(e=n.valueOf)&&!a(r=e.call(n)))return r;if(!t&&"function"==typeof(e=n.toString)&&!a(r=e.call(n)))return r;throw TypeError("Can't convert object to primitive value")}},function(n,t,e){n.exports=e(15)},function(n,t,e){"use strict";var a=e(105),r=e(50),o=e(54),i={};e(15)(i,e(9)("iterator"),(function(){return this})),n.exports=function(n,t,e){n.prototype=a(i,{next:r(1,e)}),o(n,t+" Iterator")}},function(n,t,e){var a=e(17),r=e(106),o=e(53),i=e(30)("IE_PROTO"),l=function(){},s=function(){var n,t=e(49)("iframe"),a=o.length;for(t.style.display="none",e(112).appendChild(t),t.src="javascript:",(n=t.contentWindow.document).open(),n.write(" + +`; + +function encodeBase64Url(value: string) { + return Buffer.from(value, "utf8").toString("base64url"); +} + +function generateState() { + return Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("hex"); +} diff --git a/dt-skill/src/clawpack.ts b/dt-skill/src/clawpack.ts new file mode 100644 index 00000000..7a78a33a --- /dev/null +++ b/dt-skill/src/clawpack.ts @@ -0,0 +1,145 @@ +import { gunzipSync } from "fflate"; + +type ClawPackEntry = { + path: string; + bytes: Uint8Array; +}; + +type ParsedClawPack = { + packageName: string; + packageVersion: string; + entries: ClawPackEntry[]; + packageJson: Record; + pluginManifest: Record; +}; + +const TAR_BLOCK_SIZE = 512; + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function textFromBytes(bytes: Uint8Array) { + return new TextDecoder().decode(bytes); +} + +function readTarString(block: Uint8Array, offset: number, length: number) { + const slice = block.subarray(offset, offset + length); + const end = slice.indexOf(0); + return textFromBytes(end === -1 ? slice : slice.subarray(0, end)).trim(); +} + +function readTarSize(block: Uint8Array) { + const raw = readTarString(block, 124, 12).split("\0").join("").trim(); + if (!raw) return 0; + const size = Number.parseInt(raw, 8); + if (!Number.isFinite(size) || size < 0) throw new Error("Invalid tar entry size"); + return size; +} + +function normalizeTarPath(path: string) { + const normalized = path.replaceAll("\\", "/").replace(/^\.\/+/, ""); + if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) return null; + const segments = normalized.split("/").filter(Boolean); + if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) { + return null; + } + return segments.join("/"); +} + +function isZeroBlock(block: Uint8Array) { + return block.every((byte) => byte === 0); +} + +function nextTarOffset(offset: number, size: number) { + return offset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; +} + +function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { + const entries: ClawPackEntry[] = []; + let offset = 0; + + while (offset + TAR_BLOCK_SIZE <= bytes.byteLength) { + const header = bytes.subarray(offset, offset + TAR_BLOCK_SIZE); + if (isZeroBlock(header)) break; + + const name = readTarString(header, 0, 100); + const prefix = readTarString(header, 345, 155); + const path = normalizeTarPath(prefix ? `${prefix}/${name}` : name); + if (!path) throw new Error("ClawPack contains an unsafe tar path"); + + const size = readTarSize(header); + const payloadOffset = offset + TAR_BLOCK_SIZE; + const payloadEnd = payloadOffset + size; + if (payloadEnd > bytes.byteLength) throw new Error("ClawPack tar entry is truncated"); + + const typeflag = String.fromCharCode(header[156] ?? 0).replace("\0", ""); + if (typeflag === "" || typeflag === "0") { + if (!path.startsWith("package/")) { + throw new Error("ClawPack entries must be rooted under package/"); + } + const relPath = path.slice("package/".length); + if (relPath) { + entries.push({ + path: relPath, + bytes: Uint8Array.from(bytes.subarray(payloadOffset, payloadEnd)), + }); + } + } else if (typeflag !== "5") { + throw new Error("ClawPack may only contain regular files and directories"); + } + + offset = nextTarOffset(payloadOffset, size); + } + + if (entries.length === 0) throw new Error("ClawPack contains no files"); + return entries; +} + +export function parseClawPack(bytes: Uint8Array): ParsedClawPack { + let tarBytes: Uint8Array; + try { + tarBytes = gunzipSync(bytes); + } catch { + throw new Error("ClawPack must be a gzip-compressed npm pack tarball"); + } + + const entries = parseTarEntries(tarBytes); + const packageJsonEntry = entries.find((entry) => entry.path === "package.json"); + if (!packageJsonEntry) throw new Error("ClawPack must contain package/package.json"); + const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json"); + if (!pluginManifestEntry) { + throw new Error("ClawPack must contain package/openclaw.plugin.json"); + } + + let packageJson: unknown; + try { + packageJson = JSON.parse(textFromBytes(packageJsonEntry.bytes)); + } catch { + throw new Error("ClawPack package.json is invalid JSON"); + } + if (!isRecord(packageJson)) throw new Error("ClawPack package.json must be an object"); + + const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : ""; + const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : ""; + if (!packageName) throw new Error("ClawPack package.json must declare a name"); + if (!packageVersion) throw new Error("ClawPack package.json must declare a version"); + + let pluginManifest: unknown; + try { + pluginManifest = JSON.parse(textFromBytes(pluginManifestEntry.bytes)); + } catch { + throw new Error("ClawPack openclaw.plugin.json is invalid JSON"); + } + if (!isRecord(pluginManifest)) { + throw new Error("ClawPack openclaw.plugin.json must be an object"); + } + + return { + packageName, + packageVersion, + entries, + packageJson, + pluginManifest, + }; +} diff --git a/dt-skill/src/cli.test.ts b/dt-skill/src/cli.test.ts new file mode 100644 index 00000000..336f9eb4 --- /dev/null +++ b/dt-skill/src/cli.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from "vitest"; +import { Command } from "commander"; + +describe("install command argument parsing", () => { + it("accepts multiple skill slugs via variadic argument", async () => { + const program = new Command(); + const installAction = vi.fn(); + + program + .command("install ") + .description("Install skill(s)") + .action(installAction); + + await program.parseAsync(["node", "dt-skill", "install", "skill-a", "skill-b", "skill-c"]); + + expect(installAction).toHaveBeenCalledTimes(1); + const receivedSlugs = installAction.mock.calls[0][0]; + expect(Array.isArray(receivedSlugs)).toBe(true); + expect(receivedSlugs).toEqual(["skill-a", "skill-b", "skill-c"]); + }); + + it("accepts a single skill slug", async () => { + const program = new Command(); + const installAction = vi.fn(); + + program + .command("install ") + .description("Install skill(s)") + .action(installAction); + + await program.parseAsync(["node", "dt-skill", "install", "single-skill"]); + + expect(installAction).toHaveBeenCalledTimes(1); + const receivedSlugs = installAction.mock.calls[0][0]; + expect(Array.isArray(receivedSlugs)).toBe(true); + expect(receivedSlugs).toEqual(["single-skill"]); + }); +}); diff --git a/dt-skill/src/cli.ts b/dt-skill/src/cli.ts new file mode 100644 index 00000000..e00ea676 --- /dev/null +++ b/dt-skill/src/cli.ts @@ -0,0 +1,710 @@ +#!/usr/bin/env node +import { stat } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { Command } from "commander"; +import { getCliBuildLabel, getCliVersion } from "./cli/buildInfo.js"; +import { resolveClawdbotDefaultWorkspace } from "./cli/clawdbotConfig.js"; +import { + cmdDeleteSkill, + cmdHideSkill, + cmdUndeleteSkill, + cmdUnhideSkill, +} from "./cli/commands/delete.js"; +import { cmdInspect } from "./cli/commands/inspect.js"; +import { cmdMergeSkill, cmdRenameSkill } from "./cli/commands/ownership.js"; +import { + cmdDeletePackage, + cmdDownloadPackage, + cmdExplorePackages, + cmdGetPackageTrustedPublisher, + cmdInspectPackage, + cmdPackageModerationStatus, + cmdPackageMigrationStatus, + cmdPackageReadiness, + cmdPackPackage, + cmdPublishPackage, + cmdReportPackage, + cmdTransferPackage, + cmdUndeletePackage, + cmdVerifyPackage, +} from "./cli/commands/packages.js"; +import { cmdPublish } from "./cli/commands/publish.js"; +import { cmdCreatePublisher } from "./cli/commands/publishers.js"; +import { + cmdExplore, + cmdInstall, + cmdList, + cmdPin, + cmdSearch, + cmdUninstall, + cmdUnpin, + cmdUpdate, +} from "./cli/commands/skills.js"; +import { cmdStarSkill } from "./cli/commands/star.js"; +import { cmdSync } from "./cli/commands/sync.js"; +import { + cmdTransferAccept, + cmdTransferCancel, + cmdTransferList, + cmdTransferReject, + cmdTransferRequest, +} from "./cli/commands/transfer.js"; +import { cmdUnstarSkill } from "./cli/commands/unstar.js"; +import { isAgentName, listAgentNames, resolveAgentWorkdir } from "./cli/agents.js"; +import { configureCommanderHelp, styleEnvBlock, styleTitle } from "./cli/helpStyle.js"; +import { DEFAULT_REGISTRY, DEFAULT_SITE } from "./cli/registry.js"; +import type { GlobalOpts } from "./cli/types.js"; +import { fail } from "./cli/ui.js"; + +const CLAWSCAN_NOTE_HELP = + "This note gives ClawScan context for behavior that may otherwise look unusual, such as network access, native host access, or provider-specific credentials."; + +const program = new Command() + .name("dt-skill") + .description( + `${styleTitle(`dt-skill CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock( + "install, update, search, and publish skills plus OpenClaw packages.", + )}`, + ) + .version(getCliVersion(), "-V, --cli-version", "Show CLI version") + .option("--workdir ", "Working directory (default: cwd)") + .option("--dir ", "Skills directory (relative to workdir, default: skills)") + .option("--site ", "Doraemon site URL for registry discovery") + .option("--registry ", "Registry API base URL") + .option("--agent ", `Target agent (${listAgentNames().join(", ")})`) + .option("--global", "Install skills to the global agent directory (requires --agent)") + .option("--no-input", "Disable prompts") + .showHelpAfterError() + .showSuggestionAfterError() + .addHelpText( + "after", + styleEnvBlock( + "\nEnv:\n CLAWHUB_SITE\n CLAWHUB_REGISTRY\n CLAWHUB_WORKDIR\n (CLAWDHUB_* supported)\n", + ), + ); + +configureCommanderHelp(program); + +function registerCommand(parent: Command, path: readonly string[]) { + return parent.command(path.at(-1) ?? ""); +} + +function registerCommandGroup(parent: Command, path: readonly string[]) { + return parent.command(path.at(-1) ?? ""); +} + +async function resolveGlobalOpts(): Promise { + const raw = program.opts<{ + workdir?: string; + dir?: string; + site?: string; + registry?: string; + agent?: string; + global?: boolean; + }>(); + + const rawAgent = raw.agent?.trim(); + if (rawAgent && !isAgentName(rawAgent)) { + fail(`Unknown agent "${rawAgent}". Supported: ${listAgentNames().join(", ")}`); + } + const agentName: string | undefined = rawAgent; + + const isGlobal = raw.global ?? false; + if (isGlobal && !agentName) { + fail("--global requires --agent"); + } + + let workdir: string; + let dir: string; + + if (agentName) { + workdir = resolveAgentWorkdir(agentName as import("./cli/agents.js").AgentName, isGlobal); + dir = resolve(workdir, "skills"); + } else { + workdir = await resolveWorkdir(raw.workdir); + dir = resolve(workdir, raw.dir ?? "skills"); + } + + const site = raw.site ?? process.env.CLAWHUB_SITE ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE; + const registrySource = raw.registry + ? "cli" + : process.env.CLAWHUB_REGISTRY || process.env.CLAWDHUB_REGISTRY + ? "env" + : "default"; + const registry = + raw.registry ?? + process.env.CLAWHUB_REGISTRY ?? + process.env.CLAWDHUB_REGISTRY ?? + DEFAULT_REGISTRY; + return { workdir, dir, site, registry, registrySource, agent: agentName, globalScope: isGlobal, globalScopeExplicit: raw.global !== undefined }; +} + +function isInputAllowed() { + const globalFlags = program.opts<{ input?: boolean }>(); + return globalFlags.input !== false; +} + +async function resolveWorkdir(explicit?: string) { + if (explicit?.trim()) return resolve(explicit.trim()); + const envWorkdir = process.env.CLAWHUB_WORKDIR?.trim() ?? process.env.CLAWDHUB_WORKDIR?.trim(); + if (envWorkdir) return resolve(envWorkdir); + + const cwd = resolve(process.cwd()); + const hasMarker = await hasClawhubMarker(cwd); + if (hasMarker) return cwd; + + const clawdbotWorkspace = await resolveClawdbotDefaultWorkspace(); + return clawdbotWorkspace ? resolve(clawdbotWorkspace) : cwd; +} + +async function hasClawhubMarker(workdir: string) { + const lockfile = join(workdir, ".clawhub", "lock.json"); + if (await pathExists(lockfile)) return true; + const markerDir = join(workdir, ".clawhub"); + if (await pathExists(markerDir)) return true; + const legacyLockfile = join(workdir, ".clawdhub", "lock.json"); + if (await pathExists(legacyLockfile)) return true; + const legacyMarkerDir = join(workdir, ".clawdhub"); + return pathExists(legacyMarkerDir); +} + +async function pathExists(path: string) { + try { + await stat(path); + return true; + } catch { + return false; + } +} + +registerCommand(program, ["search"]) + .description("Vector search skills") + .argument("", "Query string") + .option("--limit ", "Max results", (value) => Number.parseInt(value, 10)) + .action(async (queryParts, options) => { + const opts = await resolveGlobalOpts(); + const query = queryParts.join(" ").trim(); + await cmdSearch(opts, query, options.limit); + }); + +registerCommand(program, ["install"]) + .description("Install skill(s) into /") + .argument("", "One or more skill slugs") + .option("--version ", "Version to install (single slug only)") + .option("--force", "Overwrite existing folders") + .action(async (slugs, options) => { + const opts = await resolveGlobalOpts(); + if (options.version && slugs.length > 1) { + fail("--version requires exactly one slug"); + } + await cmdInstall(opts, slugs, options.version, options.force); + }); + +registerCommand(program, ["update"]) + .description("Update installed skills") + .argument("[slug]", "Skill slug") + .option("--all", "Update all installed skills") + .option("--version ", "Update to specific version (single slug only)") + .option("--force", "Overwrite when local files do not match any version") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUpdate(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["uninstall"]) + .description("Uninstall a skill") + .argument("", "Skill slug") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUninstall(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["list"]) + .description("List installed skills (tracked and manually installed)") + .action(async () => { + const opts = await resolveGlobalOpts(); + await cmdList(opts); + }); + +registerCommand(program, ["pin"]) + .description("Pin an installed skill so update commands skip it") + .argument("", "Skill slug") + .option("--reason ", "Optional pin reason") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdPin(opts, slug, options); + }); + +registerCommand(program, ["unpin"]) + .description("Remove a skill pin so updates can change it again") + .argument("", "Skill slug") + .action(async (slug) => { + const opts = await resolveGlobalOpts(); + await cmdUnpin(opts, slug); + }); + +registerCommand(program, ["explore"]) + .description("Browse latest updated skills from the registry") + .option( + "--limit ", + "Number of skills to show (max 200)", + (value) => Number.parseInt(value, 10), + 25, + ) + .option( + "--sort ", + "Sort by newest, downloads, rating, installs, installsAllTime, or trending", + "newest", + ) + .option("--json", "Output JSON") + .action(async (options) => { + const opts = await resolveGlobalOpts(); + const limit = + typeof options.limit === "number" && Number.isFinite(options.limit) ? options.limit : 25; + await cmdExplore(opts, { limit, sort: options.sort, json: options.json }); + }); + +registerCommand(program, ["inspect"]) + .description("Fetch skill metadata and files without installing") + .argument("", "Skill slug") + .option("--version ", "Version to inspect") + .option("--tag ", "Tag to inspect (default: latest)") + .option("--versions", "List version history (first page)") + .option("--limit ", "Max versions to list (1-200)", (value) => Number.parseInt(value, 10)) + .option("--files", "List files for the selected version") + .option("--file ", "Fetch raw file content (text <= 200KB)") + .option("--json", "Output JSON") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdInspect(opts, slug, options); + }); + +registerCommand(program, ["publish"]) + .description("Legacy alias: publish a skill from folder") + .argument("", "Skill folder path") + .option("--slug ", "Skill slug") + .option("--name ", "Display name") + .option("--owner ", "Publish under an org/user publisher handle") + .option("--migrate-owner", "Move an existing skill to the selected owner when republishing") + .option("--version ", "Version (semver)") + .option("--fork-of ", "Mark as a fork of an existing skill") + .option("--changelog ", "Changelog text") + .option("--clawscan-note ", CLAWSCAN_NOTE_HELP) + .option("--tags ", "Comma-separated tags", "latest") + .option("--all", "Batch mode: upload all discovered skills without interactive selection") + .option("--category ", "Category for batch upload") + .action(async (folder, options) => { + const opts = await resolveGlobalOpts(); + await cmdPublish(opts, folder, options); + }); + +registerCommand(program, ["delete"]) + .description("Soft-delete one of your skills") + .argument("", "Skill slug") + .option("--reason ", "Moderation note/reason") + .option("--note ", "Alias for --reason") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdDeleteSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["hide"]) + .description("Hide one of your skills") + .argument("", "Skill slug") + .option("--reason ", "Moderation note/reason") + .option("--note ", "Alias for --reason") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdHideSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["undelete"]) + .description("Restore one of your hidden skills") + .argument("", "Skill slug") + .option("--reason ", "Moderation note/reason") + .option("--note ", "Alias for --reason") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUndeleteSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["unhide"]) + .description("Unhide one of your skills") + .argument("", "Skill slug") + .option("--reason ", "Moderation note/reason") + .option("--note ", "Alias for --reason") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUnhideSkill(opts, slug, options, isInputAllowed()); + }); + +const skill = registerCommandGroup(program, ["skill"]).description("Manage published skills"); +registerCommand(skill, ["skill", "publish"]) + .description("Publish a skill from folder") + .argument("", "Skill folder path") + .option("--slug ", "Skill slug") + .option("--name ", "Display name") + .option("--owner ", "Publish under an org/user publisher handle") + .option("--migrate-owner", "Move an existing skill to the selected owner when republishing") + .option("--version ", "Version (semver)") + .option("--fork-of ", "Mark as a fork of an existing skill") + .option("--changelog ", "Changelog text") + .option("--clawscan-note ", CLAWSCAN_NOTE_HELP) + .option("--tags ", "Comma-separated tags", "latest") + .action(async (folder, options) => { + const opts = await resolveGlobalOpts(); + await cmdPublish(opts, folder, options); + }); + +const publisherCmd = registerCommandGroup(program, ["publisher"]) + .description("Publisher organization commands") + .showHelpAfterError() + .showSuggestionAfterError(); + +registerCommand(publisherCmd, ["publisher", "create"]) + .description("Create an org publisher you own") + .argument("", "Publisher handle, for example opik") + .option("--display-name ", "Publisher display name") + .option("--json", "Output JSON") + .action(async (handle, options) => { + const opts = await resolveGlobalOpts(); + await cmdCreatePublisher(opts, handle, options); + }); + +const packageCmd = registerCommandGroup(program, ["package"]).description( + "Browse and publish OpenClaw packages", +); + +registerCommand(packageCmd, ["package", "explore"]) + .description("Browse published packages and plugins") + .argument("[query...]", "Optional search query") + .option("--family ", "skill|code-plugin|bundle-plugin") + .option("--official", "Only official packages") + .option("--executes-code", "Only packages that execute code") + .option("--target ", "Filter by host target, e.g. darwin-arm64") + .option("--os ", "Filter by host OS, e.g. darwin, linux, win32") + .option("--arch ", "Filter by host architecture, e.g. arm64 or x64") + .option("--libc ", "Filter by libc, e.g. glibc or musl") + .option("--requires-browser", "Only packages that require a browser") + .option("--requires-desktop", "Only packages that require local desktop access") + .option("--requires-native-deps", "Only packages with native dependency requirements") + .option("--requires-external-service", "Only packages that require an external service") + .option("--external-service ", "Filter by named external service") + .option("--binary ", "Filter by required local binary") + .option("--os-permission ", "Filter by required OS permission") + .option("--artifact-kind ", "legacy-zip|npm-pack") + .option("--npm-mirror", "Only packages available through the npm mirror") + .option( + "--limit ", + "Number of packages to show (max 100)", + (value) => Number.parseInt(value, 10), + 25, + ) + .option("--json", "Output JSON") + .action(async (queryParts, options) => { + const opts = await resolveGlobalOpts(); + const query = Array.isArray(queryParts) ? queryParts.join(" ").trim() : ""; + await cmdExplorePackages(opts, query, options); + }); + +registerCommand(packageCmd, ["package", "inspect"]) + .description("Fetch package metadata and files without installing") + .argument("", "Package name") + .option("--version ", "Version to inspect") + .option("--tag ", "Tag to inspect (default: latest)") + .option("--versions", "List version history (first page)") + .option("--limit ", "Max versions to list (1-100)", (value) => Number.parseInt(value, 10)) + .option("--files", "List files for the selected version") + .option("--file ", "Fetch raw file content (text only)") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdInspectPackage(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "download"]) + .description("Download a package artifact and verify its published digests") + .argument("", "Package name") + .option("--version ", "Version to download") + .option("--tag ", "Tag to download (default: latest)") + .option("-o, --output ", "Output file or directory") + .option("--force", "Overwrite existing output file") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdDownloadPackage(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "verify"]) + .description("Verify a local package artifact against ClawHub or expected digests") + .argument("", "Artifact file") + .option("--package ", "Package name to resolve expected artifact metadata") + .option("--version ", "Package version to resolve") + .option("--tag ", "Package tag to resolve") + .option("--sha256 ", "Expected ClawHub SHA-256") + .option("--npm-integrity ", "Expected npm sha512 integrity") + .option("--npm-shasum ", "Expected npm shasum") + .option("--json", "Output JSON") + .action(async (file, options) => { + const opts = await resolveGlobalOpts(); + await cmdVerifyPackage(opts, file, { + ...options, + packageName: options.package, + }); + }); + +registerCommand(packageCmd, ["package", "delete"]) + .description("Soft-delete a package and all releases") + .argument("", "Package name") + .option("--yes", "Skip confirmation") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdDeletePackage(opts, name, options, isInputAllowed()); + }); + +registerCommand(packageCmd, ["package", "undelete"]) + .description("Restore a soft-deleted package and releases") + .argument("", "Package name") + .option("--yes", "Skip confirmation") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdUndeletePackage(opts, name, options, isInputAllowed()); + }); + +registerCommand(packageCmd, ["package", "transfer"]) + .description("Transfer a plugin package to another publisher") + .argument("", "Package name") + .requiredOption("--to ", "Destination publisher handle") + .option("--reason ", "Audit reason") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdTransferPackage(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "report"]) + .description("Report a package for moderator review") + .argument("", "Package name") + .option("--version ", "Package version") + .requiredOption("--reason ", "Report reason") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdReportPackage(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "moderation-status"]) + .description("Show package moderation status") + .argument("", "Package name") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdPackageModerationStatus(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "readiness"]) + .description("Check package readiness for future OpenClaw consumption") + .argument("", "Package name") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdPackageReadiness(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "migration-status"]) + .description("Show package migration status for future OpenClaw consumption") + .argument("", "Package name") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdPackageMigrationStatus(opts, name, options); + }); + +registerCommand(packageCmd, ["package", "pack"]) + .description("Create a ClawPack npm tarball from a plugin package folder") + .argument("", "Package folder path") + .option("--pack-destination ", "Directory for the generated .tgz (default: workdir)") + .option("--json", "Output JSON") + .action(async (source, options) => { + const opts = await resolveGlobalOpts(); + await cmdPackPackage(opts, source, options); + }); + +registerCommand(packageCmd, ["package", "publish"]) + .description("Publish a code plugin or bundle plugin from a folder or GitHub source") + .argument("", "Package folder path, GitHub repo (owner/repo[@ref]), or URL") + .option("--family ", "code-plugin|bundle-plugin") + .option("--name ", "Package name") + .option("--display-name ", "Display name") + .option("--owner ", "Publish under this owner/publisher handle") + .option("--version ", "Version") + .option("--changelog ", "Changelog text") + .option("--clawscan-note ", CLAWSCAN_NOTE_HELP) + .option( + "--manual-override-reason ", + "Required for manual publish when trusted publisher config exists", + ) + .option("--tags ", "Comma-separated tags", "latest") + .option("--bundle-format ", "Bundle format") + .option("--host-targets ", "Comma-separated bundle host targets") + .option("--source-repo ", "GitHub repo (owner/repo or URL)") + .option("--source-commit ", "Git commit SHA") + .option("--source-ref ", "Git ref/tag/branch") + .option("--source-path ", "Repo subpath") + .option("--dry-run", "Preview what would be published without uploading") + .option("--json", "Output JSON (for CI pipelines)") + .action(async (source, options) => { + const opts = await resolveGlobalOpts(); + await cmdPublishPackage(opts, source, options); + }); + +const trustedPublisherCmd = registerCommandGroup(packageCmd, [ + "package", + "trusted-publisher", +]).description("Manage package trusted publisher config"); + +registerCommand(trustedPublisherCmd, ["package", "trusted-publisher", "get"]) + .description("Show trusted publisher config for a package") + .argument("", "Package name") + .option("--json", "Output JSON") + .action(async (name, options) => { + const opts = await resolveGlobalOpts(); + await cmdGetPackageTrustedPublisher(opts, name, options); + }); + +registerCommand(skill, ["skill", "rename"]) + .description("Rename a published skill and keep the old slug as a redirect") + .argument("", "Current skill slug") + .argument("", "New canonical slug") + .option("--yes", "Skip confirmation") + .action(async (slug, newSlug, options) => { + const opts = await resolveGlobalOpts(); + await cmdRenameSkill(opts, slug, newSlug, options, isInputAllowed()); + }); + +registerCommand(skill, ["skill", "merge"]) + .description("Merge one owned skill into another and redirect the old slug") + .argument("", "Source skill slug") + .argument("", "Target canonical slug") + .option("--yes", "Skip confirmation") + .action(async (sourceSlug, targetSlug, options) => { + const opts = await resolveGlobalOpts(); + await cmdMergeSkill(opts, sourceSlug, targetSlug, options, isInputAllowed()); + }); + +const transfer = registerCommandGroup(program, ["transfer"]).description( + "Transfer skill ownership", +); + +registerCommand(transfer, ["transfer", "request"]) + .description("Request skill transfer to another user") + .argument("", "Skill slug") + .argument("", "Recipient handle (e.g., @username)") + .option("--message ", "Optional message for recipient") + .option("--yes", "Skip confirmation") + .action(async (slug, handle, options) => { + const opts = await resolveGlobalOpts(); + await cmdTransferRequest(opts, slug, handle, options, isInputAllowed()); + }); + +registerCommand(transfer, ["transfer", "list"]) + .description("List pending transfer requests") + .option("--outgoing", "Show outgoing transfer requests") + .action(async (options) => { + const opts = await resolveGlobalOpts(); + await cmdTransferList(opts, options); + }); + +registerCommand(transfer, ["transfer", "accept"]) + .description("Accept incoming transfer for a skill") + .argument("", "Skill slug") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdTransferAccept(opts, slug, options, isInputAllowed()); + }); + +registerCommand(transfer, ["transfer", "reject"]) + .description("Reject incoming transfer for a skill") + .argument("", "Skill slug") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdTransferReject(opts, slug, options, isInputAllowed()); + }); + +registerCommand(transfer, ["transfer", "cancel"]) + .description("Cancel outgoing transfer for a skill") + .argument("", "Skill slug") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdTransferCancel(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["star"]) + .description("Add a skill to your highlights") + .argument("", "Skill slug") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdStarSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["unstar"]) + .description("Remove a skill from your highlights") + .argument("", "Skill slug") + .option("--yes", "Skip confirmation") + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUnstarSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ["sync"]) + .description("Scan local skills and publish new/updated ones") + .option("--root ", "Extra scan roots (one or more)") + .option("--all", "Upload all new/updated skills without prompting") + .option("--dry-run", "Show what would be uploaded") + .option("--bump ", "Version bump for updates (patch|minor|major)", "patch") + .option("--changelog ", "Changelog to use for updates (non-interactive)") + .option("--tags ", "Comma-separated tags", "latest") + .option("--concurrency ", "Concurrent registry checks (default: 4)", "4") + .action(async (options) => { + const opts = await resolveGlobalOpts(); + const bump = String(options.bump ?? "patch") as "patch" | "minor" | "major"; + if (!["patch", "minor", "major"].includes(bump)) fail("--bump must be patch|minor|major"); + const concurrencyRaw = Number(options.concurrency ?? 4); + const concurrency = Number.isFinite(concurrencyRaw) ? Math.round(concurrencyRaw) : 4; + if (concurrency < 1 || concurrency > 32) fail("--concurrency must be between 1 and 32"); + await cmdSync( + opts, + { + root: options.root, + all: options.all, + dryRun: options.dryRun, + bump, + changelog: options.changelog, + tags: options.tags, + concurrency, + }, + isInputAllowed(), + ); + }); + +program.action(async () => { + program.outputHelp(); + process.exitCode = 0; +}); + +void program.parseAsync(process.argv).catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`Error: ${message}`); + process.exit(1); +}); diff --git a/dt-skill/src/cli/agents.test.ts b/dt-skill/src/cli/agents.test.ts new file mode 100644 index 00000000..ec43322a --- /dev/null +++ b/dt-skill/src/cli/agents.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from "vitest"; +import { + AGENTS, + getAgentLabel, + isAgentName, + listAgentNames, + resolveAgentWorkdir, +} from "./agents.js"; + +describe("agents", () => { + describe("isAgentName", () => { + it("returns true for known agents", () => { + expect(isAgentName("claude-code")).toBe(true); + expect(isAgentName("codex")).toBe(true); + expect(isAgentName("cursor")).toBe(true); + }); + + it("returns false for unknown agents", () => { + expect(isAgentName("unknown")).toBe(false); + expect(isAgentName("")).toBe(false); + }); + }); + + describe("listAgentNames", () => { + it("returns all agent names", () => { + const names = listAgentNames(); + expect(names).toContain("claude-code"); + expect(names).toContain("codex"); + expect(names).toContain("cursor"); + }); + }); + + describe("getAgentLabel", () => { + it("returns the label for known agents", () => { + expect(getAgentLabel("claude-code")).toBe("Claude Code"); + expect(getAgentLabel("codex")).toBe("Codex"); + expect(getAgentLabel("cursor")).toBe("Cursor"); + }); + }); + + describe("resolveAgentWorkdir", () => { + it("resolves project workdir", () => { + const dir = resolveAgentWorkdir("claude-code", false); + expect(dir).toMatch(/\.claude$/); + }); + + it("resolves global workdir with tilde", () => { + const dir = resolveAgentWorkdir("claude-code", true); + expect(dir).not.toContain("~"); + expect(dir).toMatch(/\.claude$/); + }); + + it("resolves codex project workdir", () => { + const dir = resolveAgentWorkdir("codex", false); + expect(dir).toMatch(/\.codex$/); + }); + + it("resolves cursor global workdir", () => { + const dir = resolveAgentWorkdir("cursor", true); + expect(dir).not.toContain("~"); + expect(dir).toMatch(/\.cursor$/); + }); + }); + + describe("AGENTS config", () => { + it("has consistent structure for all agents", () => { + for (const [key, agent] of Object.entries(AGENTS)) { + expect(agent.name).toBe(key); + expect(agent.label).toBeTruthy(); + expect(agent.projectWorkdir).toBeTruthy(); + expect(agent.globalWorkdir).toMatch(/^~/); + } + }); + }); +}); diff --git a/dt-skill/src/cli/agents.ts b/dt-skill/src/cli/agents.ts new file mode 100644 index 00000000..67f5cc60 --- /dev/null +++ b/dt-skill/src/cli/agents.ts @@ -0,0 +1,51 @@ +import { join } from "node:path"; +import { resolveHome } from "../homedir.js"; + +export const AGENTS = { + "claude-code": { + name: "claude-code", + label: "Claude Code", + projectWorkdir: ".claude", + globalWorkdir: "~/.claude", + }, + codex: { + name: "codex", + label: "Codex", + projectWorkdir: ".codex", + globalWorkdir: "~/.codex", + }, + cursor: { + name: "cursor", + label: "Cursor", + projectWorkdir: ".cursor", + globalWorkdir: "~/.cursor", + }, +} as const; + +export type AgentName = keyof typeof AGENTS; + +export function isAgentName(value: string): value is AgentName { + return value in AGENTS; +} + +export function listAgentNames(): AgentName[] { + return Object.keys(AGENTS) as AgentName[]; +} + +function resolveTilde(path: string): string { + if (path.startsWith("~/")) { + return join(resolveHome(), path.slice(2)); + } + return path; +} + +export function resolveAgentWorkdir(agentName: AgentName, isGlobal: boolean): string { + const agent = AGENTS[agentName]; + if (!agent) throw new Error(`Unknown agent: ${agentName}`); + const raw = isGlobal ? agent.globalWorkdir : agent.projectWorkdir; + return resolveTilde(raw); +} + +export function getAgentLabel(agentName: AgentName): string { + return AGENTS[agentName]?.label ?? agentName; +} diff --git a/dt-skill/src/cli/authToken.ts b/dt-skill/src/cli/authToken.ts new file mode 100644 index 00000000..19591e78 --- /dev/null +++ b/dt-skill/src/cli/authToken.ts @@ -0,0 +1,13 @@ +import { readGlobalConfig } from "../config.js"; +import { fail } from "./ui.js"; + +export async function getOptionalAuthToken(): Promise { + const cfg = await readGlobalConfig(); + return cfg?.token ?? undefined; +} + +export async function requireAuthToken(): Promise { + const token = await getOptionalAuthToken(); + if (!token) fail("Not logged in. Run: clawhub login"); + return token; +} diff --git a/dt-skill/src/cli/buildInfo.ts b/dt-skill/src/cli/buildInfo.ts new file mode 100644 index 00000000..c3b09a2c --- /dev/null +++ b/dt-skill/src/cli/buildInfo.ts @@ -0,0 +1,95 @@ +import { existsSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +type PackageJson = { version?: string }; + +function readPackageVersion() { + try { + const path = join(dirname(fileURLToPath(import.meta.url)), "../../package.json"); + const raw = readFileSync(path, "utf8"); + const pkg = JSON.parse(raw) as PackageJson; + return typeof pkg.version === "string" ? pkg.version : "0.0.0"; + } catch { + return "0.0.0"; + } +} + +function shortCommit(value: string) { + const trimmed = value.trim(); + if (!trimmed) return null; + if (trimmed.length <= 8) return trimmed; + return trimmed.slice(0, 8); +} + +function getCliCommit() { + const candidates = [ + process.env.CLAWHUB_COMMIT, + process.env.CLAWDHUB_COMMIT, + process.env.VERCEL_GIT_COMMIT_SHA, + process.env.GITHUB_SHA, + process.env.COMMIT_SHA, + ]; + for (const candidate of candidates) { + if (!candidate) continue; + const short = shortCommit(candidate); + if (short) return short; + } + return readGitCommitFromCwd(); +} + +export function getCliVersion() { + return readPackageVersion(); +} + +export function getCliBuildLabel() { + const version = getCliVersion(); + const commit = getCliCommit(); + return commit ? `v${version} (${commit})` : `v${version}`; +} + +function readGitCommitFromCwd() { + try { + const gitDir = findGitDir(process.cwd()); + if (!gitDir) return null; + const headPath = join(gitDir, "HEAD"); + if (!existsSync(headPath)) return null; + const head = readFileSync(headPath, "utf8").trim(); + if (!head) return null; + if (!head.startsWith("ref:")) return shortCommit(head); + const ref = head.replace(/^ref:\s*/, "").trim(); + if (!ref) return null; + const refPath = join(gitDir, ref); + if (!existsSync(refPath)) return null; + const sha = readFileSync(refPath, "utf8").trim(); + return shortCommit(sha); + } catch { + return null; + } +} + +function findGitDir(start: string) { + let current = resolve(start); + for (;;) { + const dotGit = join(current, ".git"); + if (existsSync(dotGit)) { + try { + const stat = statSync(dotGit); + if (stat.isDirectory()) return dotGit; + } catch { + // ignore + } + try { + const content = readFileSync(dotGit, "utf8").trim(); + const match = content.match(/^gitdir:\s*(.+)$/); + if (match?.[1]) return resolve(current, match[1]); + } catch { + return dotGit; + } + return dotGit; + } + const parent = resolve(current, ".."); + if (parent === current) return null; + current = parent; + } +} diff --git a/dt-skill/src/cli/clawdbotConfig.test.ts b/dt-skill/src/cli/clawdbotConfig.test.ts new file mode 100644 index 00000000..ef33f0eb --- /dev/null +++ b/dt-skill/src/cli/clawdbotConfig.test.ts @@ -0,0 +1,238 @@ +/* @vitest-environment node */ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { resolveHome } from "../homedir.js"; +import { resolveClawdbotDefaultWorkspace, resolveClawdbotSkillRoots } from "./clawdbotConfig.js"; + +const originalEnv = { ...process.env }; + +afterEach(() => { + process.env = { ...originalEnv }; +}); + +describe("resolveClawdbotSkillRoots", () => { + it("reads JSON5 config and resolves per-agent + shared skill roots", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-")); + const home = join(base, "home"); + const stateDir = join(base, "state"); + const configPath = join(base, "clawdbot.json"); + const openclawStateDir = join(base, "openclaw-state"); + + process.env.HOME = home; + process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_STATE_DIR = openclawStateDir; + process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); + + const config = `{ + // JSON5 comments + trailing commas supported + agents: { + defaults: { workspace: '~/clawd-main', }, + list: [ + { id: 'work', name: 'Work Bot', workspace: '~/clawd-work', }, + { id: 'family', workspace: '~/clawd-family', }, + ], + }, + // legacy entries still supported + agent: { workspace: '~/clawd-legacy', }, + routing: { + agents: { + work: { name: 'Work Bot', workspace: '~/clawd-work', }, + family: { workspace: '~/clawd-family' }, + }, + }, + skills: { + load: { extraDirs: ['~/shared/skills', '/opt/skills',], }, + }, + }`; + await writeFile(configPath, config, "utf8"); + + const { roots, labels } = await resolveClawdbotSkillRoots(); + + const expectedRoots = [ + resolve(stateDir, "skills"), + resolve(openclawStateDir, "skills"), + resolve(home, "clawd-main", "skills"), + resolve(home, "clawd-work", "skills"), + resolve(home, "clawd-family", "skills"), + resolve(home, "shared", "skills"), + resolve("/opt/skills"), + ]; + + expect(roots).toEqual(expect.arrayContaining(expectedRoots)); + expect(labels[resolve(stateDir, "skills")]).toBe("Shared skills"); + expect(labels[resolve(openclawStateDir, "skills")]).toBe("OpenClaw: Shared skills"); + expect(labels[resolve(home, "clawd-main", "skills")]).toBe("Agent: main"); + expect(labels[resolve(home, "clawd-work", "skills")]).toBe("Agent: Work Bot"); + expect(labels[resolve(home, "clawd-family", "skills")]).toBe("Agent: family"); + expect(labels[resolve(home, "shared", "skills")]).toBe("Extra: skills"); + expect(labels[resolve("/opt/skills")]).toBe("Extra: skills"); + }); + + it("resolves default workspace from agents.defaults and agents.list", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-default-")); + const home = join(base, "home"); + const stateDir = join(base, "state"); + const configPath = join(base, "clawdbot.json"); + const workspaceMain = join(base, "workspace-main"); + const workspaceList = join(base, "workspace-list"); + const openclawStateDir = join(base, "openclaw-state"); + + process.env.HOME = home; + process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_STATE_DIR = openclawStateDir; + process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); + + const config = `{ + agents: { + defaults: { workspace: "${workspaceMain}", }, + list: [ + { id: 'main', workspace: "${workspaceList}", default: true }, + ], + }, + }`; + await writeFile(configPath, config, "utf8"); + + const workspace = await resolveClawdbotDefaultWorkspace(); + expect(workspace).toBe(resolve(workspaceMain)); + }); + + it("falls back to default agent in agents.list when defaults missing", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-list-")); + const home = join(base, "home"); + const configPath = join(base, "clawdbot.json"); + const workspaceMain = join(base, "workspace-main"); + const workspaceWork = join(base, "workspace-work"); + const openclawStateDir = join(base, "openclaw-state"); + + process.env.HOME = home; + process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_STATE_DIR = openclawStateDir; + process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); + + const config = `{ + agents: { + list: [ + { id: 'main', workspace: "${workspaceMain}", default: true }, + { id: 'work', workspace: "${workspaceWork}" }, + ], + }, + }`; + await writeFile(configPath, config, "utf8"); + + const workspace = await resolveClawdbotDefaultWorkspace(); + expect(workspace).toBe(resolve(workspaceMain)); + }); + + it("respects CLAWDBOT_STATE_DIR and CLAWDBOT_CONFIG_PATH overrides", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-override-")); + const home = join(base, "home"); + const stateDir = join(base, "custom-state"); + const configPath = join(base, "config", "clawdbot.json"); + const openclawStateDir = join(base, "openclaw-state"); + + process.env.HOME = home; + process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_STATE_DIR = openclawStateDir; + process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); + + const config = `{ + agent: { workspace: "${join(base, "workspace-main")}" }, + }`; + await mkdir(join(base, "config"), { recursive: true }); + await writeFile(configPath, config, "utf8"); + + const { roots, labels } = await resolveClawdbotSkillRoots(); + + expect(roots).toEqual( + expect.arrayContaining([ + resolve(stateDir, "skills"), + resolve(openclawStateDir, "skills"), + resolve(join(base, "workspace-main"), "skills"), + ]), + ); + expect(labels[resolve(stateDir, "skills")]).toBe("Shared skills"); + expect(labels[resolve(openclawStateDir, "skills")]).toBe("OpenClaw: Shared skills"); + expect(labels[resolve(join(base, "workspace-main"), "skills")]).toBe("Agent: main"); + }); + + it("returns shared skills root when config is missing", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-missing-")); + const stateDir = join(base, "state"); + const configPath = join(base, "missing", "clawdbot.json"); + const openclawStateDir = join(base, "openclaw-state"); + + process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_STATE_DIR = openclawStateDir; + process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); + + const { roots, labels } = await resolveClawdbotSkillRoots(); + + expect(roots).toEqual([resolve(stateDir, "skills"), resolve(openclawStateDir, "skills")]); + expect(labels[resolve(stateDir, "skills")]).toBe("Shared skills"); + expect(labels[resolve(openclawStateDir, "skills")]).toBe("OpenClaw: Shared skills"); + }); + + it("uses $HOME over os.homedir() for tilde expansion", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-home-override-")); + const customHome = join(base, "custom-home"); + const stateDir = join(base, "state"); + const configPath = join(base, "clawdbot.json"); + const openclawStateDir = join(base, "openclaw-state"); + + process.env.HOME = customHome; + process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.CLAWDBOT_CONFIG_PATH = configPath; + process.env.OPENCLAW_STATE_DIR = openclawStateDir; + process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); + + const config = `{ + agents: { + defaults: { workspace: "~/my-workspace" }, + }, + }`; + await writeFile(configPath, config, "utf8"); + + const workspace = await resolveClawdbotDefaultWorkspace(); + expect(workspace).toBe(resolve(customHome, "my-workspace")); + expect(resolveHome()).toBe(customHome); + }); + + it("normalizes trailing separators in $HOME", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-home-trailing-")); + const customHome = join(base, "custom-home"); + + process.env.HOME = `${customHome}/`; + + expect(resolveHome()).toBe(customHome); + }); + + it("supports OpenClaw configuration files", async () => { + const base = await mkdtemp(join(tmpdir(), "clawhub-openclaw-")); + const stateDir = join(base, "openclaw-state"); + const workspace = join(base, "openclaw-main"); + const configPath = join(stateDir, "openclaw.json"); + + process.env.OPENCLAW_STATE_DIR = stateDir; + + await mkdir(stateDir, { recursive: true }); + const config = `{ + agents: { + defaults: { workspace: "${workspace}", }, + }, + }`; + await writeFile(configPath, config, "utf8"); + + const { roots, labels } = await resolveClawdbotSkillRoots(); + expect(roots).toEqual( + expect.arrayContaining([resolve(stateDir, "skills"), resolve(workspace, "skills")]), + ); + expect(labels[resolve(stateDir, "skills")]).toBe("OpenClaw: Shared skills"); + expect(labels[resolve(workspace, "skills")]).toBe("OpenClaw: Agent: main"); + }); +}); diff --git a/dt-skill/src/cli/clawdbotConfig.ts b/dt-skill/src/cli/clawdbotConfig.ts new file mode 100644 index 00000000..4decb342 --- /dev/null +++ b/dt-skill/src/cli/clawdbotConfig.ts @@ -0,0 +1,204 @@ +import { readFile } from "node:fs/promises"; +import { basename, join, resolve } from "node:path"; +import JSON5 from "json5"; +import { resolveHome } from "../homedir.js"; + +type ClawdbotConfig = { + agent?: { workspace?: string }; + agents?: { + defaults?: { workspace?: string }; + list?: Array<{ + id?: string; + name?: string; + workspace?: string; + default?: boolean; + }>; + }; + routing?: { + agents?: Record< + string, + { + name?: string; + workspace?: string; + } + >; + }; + skills?: { + load?: { + extraDirs?: string[]; + }; + }; +}; + +type ClawdbotSkillRoots = { + roots: string[]; + labels: Record; +}; + +export async function resolveClawdbotSkillRoots(): Promise { + const roots: string[] = []; + const labels: Record = {}; + + const clawdbotStateDir = resolveClawdbotStateDir(); + const sharedSkills = resolveUserPath(join(clawdbotStateDir, "skills")); + pushRoot(roots, labels, sharedSkills, "Shared skills"); + + const openclawStateDir = resolveOpenclawStateDir(); + const openclawShared = resolveUserPath(join(openclawStateDir, "skills")); + pushRoot(roots, labels, openclawShared, "OpenClaw: Shared skills"); + + const [clawdbotConfig, openclawConfig] = await Promise.all([ + readClawdbotConfig(), + readOpenclawConfig(), + ]); + if (!clawdbotConfig && !openclawConfig) return { roots, labels }; + + if (clawdbotConfig) { + addConfigRoots(clawdbotConfig, roots, labels); + } + if (openclawConfig) { + addConfigRoots(openclawConfig, roots, labels, "OpenClaw"); + } + + return { roots, labels }; +} + +export async function resolveClawdbotDefaultWorkspace(): Promise { + const config = await readClawdbotConfig(); + const openclawConfig = await readOpenclawConfig(); + if (!config && !openclawConfig) return null; + + const defaultsWorkspace = resolveUserPath( + config?.agents?.defaults?.workspace ?? config?.agent?.workspace ?? "", + ); + if (defaultsWorkspace) return defaultsWorkspace; + + const listedAgents = config?.agents?.list ?? []; + const defaultAgent = + listedAgents.find((entry) => entry.default) ?? + listedAgents.find((entry) => entry.id === "main"); + const listWorkspace = resolveUserPath(defaultAgent?.workspace ?? ""); + if (listWorkspace) return listWorkspace; + + if (!openclawConfig) return null; + const openclawDefaults = resolveUserPath( + openclawConfig.agents?.defaults?.workspace ?? openclawConfig.agent?.workspace ?? "", + ); + if (openclawDefaults) return openclawDefaults; + const openclawAgents = openclawConfig.agents?.list ?? []; + const openclawDefaultAgent = + openclawAgents.find((entry) => entry.default) ?? + openclawAgents.find((entry) => entry.id === "main"); + const openclawWorkspace = resolveUserPath(openclawDefaultAgent?.workspace ?? ""); + return openclawWorkspace || null; +} + +function resolveClawdbotStateDir() { + const override = process.env.CLAWDBOT_STATE_DIR?.trim(); + if (override) return resolveUserPath(override); + return join(resolveHome(), ".clawdbot"); +} + +function resolveClawdbotConfigPath() { + const override = process.env.CLAWDBOT_CONFIG_PATH?.trim(); + if (override) return resolveUserPath(override); + return join(resolveClawdbotStateDir(), "clawdbot.json"); +} + +function resolveOpenclawStateDir() { + const override = process.env.OPENCLAW_STATE_DIR?.trim(); + if (override) return resolveUserPath(override); + return join(resolveHome(), ".openclaw"); +} + +function resolveOpenclawConfigPath() { + const override = process.env.OPENCLAW_CONFIG_PATH?.trim(); + if (override) return resolveUserPath(override); + return join(resolveOpenclawStateDir(), "openclaw.json"); +} + +function resolveUserPath(input: string) { + const trimmed = input.trim(); + if (!trimmed) return ""; + if (trimmed.startsWith("~")) { + return resolve(trimmed.replace(/^~(?=$|[\\/])/, resolveHome())); + } + return resolve(trimmed); +} + +async function readClawdbotConfig(): Promise { + return readConfigFile(resolveClawdbotConfigPath()); +} + +async function readOpenclawConfig(): Promise { + return readConfigFile(resolveOpenclawConfigPath()); +} + +async function readConfigFile(path: string): Promise { + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON5.parse(raw); + if (!parsed || typeof parsed !== "object") return null; + return parsed as ClawdbotConfig; + } catch { + return null; + } +} + +function addConfigRoots( + config: ClawdbotConfig, + roots: string[], + labels: Record, + labelPrefix?: string, +) { + const prefix = labelPrefix ? `${labelPrefix}: ` : ""; + + const mainWorkspace = resolveUserPath( + config.agents?.defaults?.workspace ?? config.agent?.workspace ?? "", + ); + if (mainWorkspace) { + pushRoot(roots, labels, join(mainWorkspace, "skills"), `${prefix}Agent: main`); + } + + const listedAgents = config.agents?.list ?? []; + for (const entry of listedAgents) { + const workspace = resolveUserPath(entry?.workspace ?? ""); + if (!workspace) continue; + const name = entry?.name?.trim() || entry?.id?.trim() || "agent"; + pushRoot(roots, labels, join(workspace, "skills"), `${prefix}Agent: ${name}`); + } + + const agents = config.routing?.agents ?? {}; + for (const [agentId, entry] of Object.entries(agents)) { + const workspace = resolveUserPath(entry?.workspace ?? ""); + if (!workspace) continue; + const name = entry?.name?.trim() || agentId; + pushRoot(roots, labels, join(workspace, "skills"), `${prefix}Agent: ${name}`); + } + + const extraDirs = config.skills?.load?.extraDirs ?? []; + for (const dir of extraDirs) { + const resolved = resolveUserPath(dir); + if (!resolved) continue; + const label = `${prefix}Extra: ${basename(resolved) || resolved}`; + pushRoot(roots, labels, resolved, label); + } +} + +function pushRoot(roots: string[], labels: Record, root: string, label?: string) { + const resolved = resolveUserPath(root); + if (!resolved) return; + if (!roots.includes(resolved)) roots.push(resolved); + if (!label) return; + const existing = labels[resolved]; + if (!existing) { + labels[resolved] = label; + return; + } + const parts = existing + .split(", ") + .map((part) => part.trim()) + .filter(Boolean); + if (parts.includes(label)) return; + labels[resolved] = `${existing}, ${label}`; +} diff --git a/dt-skill/src/cli/commands/auth.test.ts b/dt-skill/src/cli/commands/auth.test.ts new file mode 100644 index 00000000..749fa530 --- /dev/null +++ b/dt-skill/src/cli/commands/auth.test.ts @@ -0,0 +1,56 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createRegistryModuleMocks, makeGlobalOpts } from "../../../test/cliCommandTestKit.js"; + +const mockReadGlobalConfig = vi.fn( + async () => null as { registry?: string; token?: string } | null, +); +const mockWriteGlobalConfig = vi.fn(async (_cfg: unknown) => {}); +vi.mock("../../config.js", () => ({ + readGlobalConfig: () => mockReadGlobalConfig(), + writeGlobalConfig: (cfg: unknown) => mockWriteGlobalConfig(cfg), +})); + +const registryMocks = createRegistryModuleMocks(); +const mockGetRegistry = registryMocks.getRegistry; +vi.mock("../registry.js", () => registryMocks.moduleFactory()); + +const { cmdLogout } = await import("./auth"); + +const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); + +afterEach(() => { + vi.clearAllMocks(); + mockLog.mockClear(); +}); + +describe("cmdLogout", () => { + it("removes token and logs a clear message", async () => { + mockReadGlobalConfig.mockResolvedValueOnce({ registry: "https://clawhub.ai", token: "tkn" }); + + await cmdLogout(makeGlobalOpts()); + + expect(mockWriteGlobalConfig).toHaveBeenCalledWith({ + registry: "https://clawhub.ai", + token: undefined, + }); + expect(mockGetRegistry).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledWith( + "OK. Logged out locally. Token still valid until revoked (Settings -> API tokens).", + ); + }); + + it("falls back to resolved registry when config has no registry", async () => { + mockReadGlobalConfig.mockResolvedValueOnce({ token: "tkn" }); + mockGetRegistry.mockResolvedValueOnce("https://registry.example"); + + await cmdLogout(makeGlobalOpts()); + + expect(mockGetRegistry).toHaveBeenCalled(); + expect(mockWriteGlobalConfig).toHaveBeenCalledWith({ + registry: "https://registry.example", + token: undefined, + }); + }); +}); diff --git a/dt-skill/src/cli/commands/auth.ts b/dt-skill/src/cli/commands/auth.ts new file mode 100644 index 00000000..0ea311fd --- /dev/null +++ b/dt-skill/src/cli/commands/auth.ts @@ -0,0 +1,145 @@ +import { buildCliAuthUrl, startLoopbackAuthServer } from "../../browserAuth.js"; +import { readGlobalConfig, writeGlobalConfig } from "../../config.js"; +import { pollForDeviceToken, requestDeviceCode } from "../../deviceAuth.js"; +import { discoverRegistryFromSite } from "../../discovery.js"; +import { apiRequest } from "../../http.js"; +import { ApiRoutes, ApiV1WhoamiResponseSchema } from "../../schema/index.js"; +import { requireAuthToken } from "../authToken.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, openInBrowser, promptHidden } from "../ui.js"; + +export async function cmdLoginFlow( + opts: GlobalOpts, + options: { token?: string; label?: string; browser?: boolean; device?: boolean }, + inputAllowed: boolean, +) { + if (options.token) { + await cmdLogin(opts, options.token, inputAllowed); + return; + } + + if (options.device) { + await cmdDeviceLogin(opts); + return; + } + + if (options.browser === false) { + fail("Token required (use --token, --device, or remove --no-browser)"); + } + + const label = (options.label ?? "CLI token").trim() || "CLI token"; + const receiver = await startLoopbackAuthServer(); + const discovery = await discoverRegistryFromSite(opts.site).catch(() => null); + const authBase = discovery?.authBase?.trim() || opts.site; + const authUrl = buildCliAuthUrl({ + siteUrl: authBase, + redirectUri: receiver.redirectUri, + label, + state: receiver.state, + }); + + console.log(`Opening browser: ${authUrl}`); + openInBrowser(authUrl); + + const result = await receiver.waitForResult(); + const registry = result.registry?.trim() || opts.registry; + const registrySource = result.registry?.trim() ? "cli" : opts.registrySource; + await cmdLogin({ ...opts, registry, registrySource }, result.token, inputAllowed); +} + +async function cmdLogin(opts: GlobalOpts, tokenFlag: string | undefined, inputAllowed: boolean) { + if (!tokenFlag && !inputAllowed) fail("Token required (use --token or remove --no-input)"); + + const token = tokenFlag || (await promptHidden("ClawHub token: ")); + if (!token) fail("Token required"); + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Verifying token"); + try { + const whoami = await apiRequest( + registry, + { method: "GET", path: ApiRoutes.whoami, token }, + ApiV1WhoamiResponseSchema, + ); + if (!whoami.user) fail("Login failed"); + + await writeGlobalConfig({ registry, token }); + const handle = whoami.user.handle ? `@${whoami.user.handle}` : "unknown user"; + spinner.succeed(`OK. Logged in as ${handle}.`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdLogout(opts: GlobalOpts) { + const cfg = await readGlobalConfig(); + const registry = cfg?.registry || (await getRegistry(opts, { cache: true })); + await writeGlobalConfig({ registry, token: undefined }); + console.log("OK. Logged out locally. Token still valid until revoked (Settings -> API tokens)."); +} + +export async function cmdWhoami(opts: GlobalOpts) { + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + + const spinner = createSpinner("Checking token"); + try { + const whoami = await apiRequest( + registry, + { method: "GET", path: ApiRoutes.whoami, token }, + ApiV1WhoamiResponseSchema, + ); + spinner.succeed(whoami.user.handle ?? "unknown"); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +/** + * Device Flow login for headless environments. + * Requests a device code, displays it to the user, then polls until authorized. + */ +export async function cmdDeviceLogin(opts: GlobalOpts) { + const discovery = await discoverRegistryFromSite(opts.site).catch(() => null); + const authBase = discovery?.authBase?.trim() || opts.site; + const registry = await getRegistry(opts, { cache: true }); + + const spinner = createSpinner("Requesting device code"); + let deviceCode; + try { + deviceCode = await requestDeviceCode({ apiUrl: registry, siteUrl: authBase }); + spinner.succeed("Device code received"); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } + + // Display the code and URL for the user + console.log(); + console.log(" To authenticate, visit:"); + console.log(` ${deviceCode.verification_uri}`); + console.log(); + console.log(` And enter code: ${deviceCode.user_code}`); + console.log(); + console.log(` Code expires in ${Math.floor(deviceCode.expires_in / 60)} minutes.`); + console.log(); + + const pollSpinner = createSpinner("Waiting for authorization"); + try { + const tokenResponse = await pollForDeviceToken( + { apiUrl: registry, siteUrl: authBase }, + deviceCode.device_code, + { interval: deviceCode.interval, expiresIn: deviceCode.expires_in }, + ); + pollSpinner.succeed("Authorized"); + + // Store the token + await cmdLogin({ ...opts, registry, registrySource: "cli" }, tokenResponse.access_token, true); + } catch (error) { + pollSpinner.fail(formatError(error)); + throw error; + } +} diff --git a/dt-skill/src/cli/commands/delete.test.ts b/dt-skill/src/cli/commands/delete.test.ts new file mode 100644 index 00000000..feca4060 --- /dev/null +++ b/dt-skill/src/cli/commands/delete.test.ts @@ -0,0 +1,136 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); + +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../ui.js", () => uiMocks.moduleFactory()); + +const { cmdDeleteSkill, cmdHideSkill, cmdUndeleteSkill, cmdUnhideSkill } = await import("./delete"); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("delete/undelete", () => { + it("requires --yes when input is disabled", async () => { + await expect(cmdDeleteSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); + await expect(cmdUndeleteSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); + await expect(cmdHideSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); + await expect(cmdUnhideSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); + }); + + it("calls delete endpoint with --yes", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdDeleteSkill(makeGlobalOpts(), "demo", { yes: true }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.not.objectContaining({ token: expect.anything() }), + expect.anything(), + ); + expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); + }); + + it("prints the slug reservation expiry returned by delete", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + slugReservedUntil: 1_700_086_400_000, + }); + await cmdDeleteSkill(makeGlobalOpts(), "demo", { yes: true }, false); + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + "OK. Deleted demo. Slug reserved until 2023-11-15T22:13:20.000Z", + ); + }); + + it("passes a moderation reason on delete", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdDeleteSkill(makeGlobalOpts(), "demo", { yes: true, reason: "legal hold" }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "DELETE", + path: "/api/v1/skills/demo", + body: { reason: "legal hold" }, + }), + expect.anything(), + ); + }); + + it("supports --note as a reason alias", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdHideSkill(makeGlobalOpts(), "demo", { yes: true, note: "legal notice" }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "DELETE", + path: "/api/v1/skills/demo", + body: { reason: "legal notice" }, + }), + expect.anything(), + ); + }); + + it("rejects conflicting reason aliases", async () => { + await expect( + cmdHideSkill( + makeGlobalOpts(), + "demo", + { yes: true, reason: "legal hold", note: "different" }, + false, + ), + ).rejects.toThrow(/only one/i); + }); + + it("calls undelete endpoint with --yes", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdUndeleteSkill(makeGlobalOpts(), "demo", { yes: true }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/undelete" }), + expect.anything(), + ); + }); + + it("passes a moderation reason on undelete", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdUndeleteSkill(makeGlobalOpts(), "demo", { yes: true, reason: "reviewed" }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "POST", + path: "/api/v1/skills/demo/undelete", + body: { reason: "reviewed" }, + }), + expect.anything(), + ); + }); + + it("supports hide/unhide aliases", async () => { + httpMocks.apiRequest.mockResolvedValue({ ok: true }); + await cmdHideSkill(makeGlobalOpts(), "demo", { yes: true }, false); + await cmdUnhideSkill(makeGlobalOpts(), "demo", { yes: true }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: "DELETE", path: "/api/v1/skills/demo" }), + expect.anything(), + ); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/undelete" }), + expect.anything(), + ); + }); +}); diff --git a/dt-skill/src/cli/commands/delete.ts b/dt-skill/src/cli/commands/delete.ts new file mode 100644 index 00000000..8051ace1 --- /dev/null +++ b/dt-skill/src/cli/commands/delete.ts @@ -0,0 +1,162 @@ +import { apiRequest } from "../../http.js"; +import { ApiRoutes, ApiV1DeleteResponseSchema, parseArk } from "../../schema/index.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; + +type SkillActionLabels = { + verb: string; + progress: string; + past: string; + promptSuffix?: string; +}; + +type SkillDeleteOptions = { + yes?: boolean; + reason?: string; + note?: string; +}; + +const deleteLabels: SkillActionLabels = { + verb: "Delete", + progress: "Deleting", + past: "Deleted", + promptSuffix: "soft delete; owner slug reservation expires after 30 days", +}; + +const undeleteLabels: SkillActionLabels = { + verb: "Undelete", + progress: "Undeleting", + past: "Undeleted", + promptSuffix: "owner/moderator/admin", +}; + +const hideLabels: SkillActionLabels = { + verb: "Hide", + progress: "Hiding", + past: "Hidden", + promptSuffix: "owner/moderator/admin", +}; + +const unhideLabels: SkillActionLabels = { + verb: "Unhide", + progress: "Unhiding", + past: "Unhidden", + promptSuffix: "owner/moderator/admin", +}; + +export async function cmdDeleteSkill( + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean, + labels: SkillActionLabels = deleteLabels, +) { + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail("Slug required"); + const reason = normalizeReason(options); + const allowPrompt = isInteractive() && inputAllowed !== false; + + if (!options.yes) { + if (!allowPrompt) fail("Pass --yes (no input)"); + const ok = await promptConfirm(formatPrompt(labels, slug)); + if (!ok) return undefined; + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`${labels.progress} ${slug}`); + try { + const result = await apiRequest( + registry, + { + method: "DELETE", + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}`, + body: reason ? { reason } : undefined, + }, + ApiV1DeleteResponseSchema, + ); + const parsed = parseArk(ApiV1DeleteResponseSchema, result, "Delete response"); + spinner.succeed(`OK. ${labels.past} ${slug}${formatSlugReservation(parsed)}`); + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdUndeleteSkill( + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean, + labels: SkillActionLabels = undeleteLabels, +) { + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail("Slug required"); + const reason = normalizeReason(options); + const allowPrompt = isInteractive() && inputAllowed !== false; + + if (!options.yes) { + if (!allowPrompt) fail("Pass --yes (no input)"); + const ok = await promptConfirm(formatPrompt(labels, slug)); + if (!ok) return undefined; + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`${labels.progress} ${slug}`); + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/undelete`, + body: reason ? { reason } : undefined, + }, + ApiV1DeleteResponseSchema, + ); + spinner.succeed(`OK. ${labels.past} ${slug}`); + return parseArk(ApiV1DeleteResponseSchema, result, "Undelete response"); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdHideSkill( + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean, +) { + return cmdDeleteSkill(opts, slugArg, options, inputAllowed, hideLabels); +} + +export async function cmdUnhideSkill( + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean, +) { + return cmdUndeleteSkill(opts, slugArg, options, inputAllowed, unhideLabels); +} + +function normalizeReason(options: SkillDeleteOptions) { + const reason = options.reason?.trim(); + const note = options.note?.trim(); + if (reason && note && reason !== note) fail("Pass only one of --reason or --note"); + const value = reason || note; + if ((options.reason !== undefined || options.note !== undefined) && !value) { + fail("--reason cannot be empty"); + } + return value; +} + +function formatPrompt(labels: SkillActionLabels, slug: string) { + const suffix = labels.promptSuffix ? ` (${labels.promptSuffix})` : ""; + return `${labels.verb} ${slug}?${suffix}`; +} + +function formatSlugReservation(result: { slugReservedUntil?: number }) { + if (typeof result.slugReservedUntil !== "number") return ""; + return `. Slug reserved until ${new Date(result.slugReservedUntil).toISOString()}`; +} diff --git a/dt-skill/src/cli/commands/github.test.ts b/dt-skill/src/cli/commands/github.test.ts new file mode 100644 index 00000000..076844aa --- /dev/null +++ b/dt-skill/src/cli/commands/github.test.ts @@ -0,0 +1,410 @@ +/* @vitest-environment node */ + +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { zipSync } from "fflate"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { fetchGitHubSource, resolveLocalGitInfo, resolveSourceInput } from "./github"; + +async function makeTmpDir() { + return await mkdtemp(join(tmpdir(), "clawhub-github-test-")); +} + +function runGit(cwd: string, args: string[]) { + const result = spawnSync("git", ["-C", cwd, ...args], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); + } + return result.stdout.trim(); +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function mockGitHubCommitLookup(validRefs: string[]) { + const originalFetch = globalThis.fetch; + const fetchMock = vi.fn(async (input) => { + const url = input instanceof Request ? input.url : input.toString(); + const match = url.match(/\/repos\/owner\/repo\/commits\/(.+)$/); + if (!match) { + throw new Error(`Unexpected fetch: ${url}`); + } + const ref = decodeURIComponent(match[1] ?? ""); + if (!validRefs.includes(ref)) { + return new Response("not found", { status: 404 }); + } + return new Response(JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }); + Object.defineProperty(globalThis, "fetch", { + value: fetchMock, + configurable: true, + writable: true, + }); + return () => { + Object.defineProperty(globalThis, "fetch", { + value: originalFetch, + configurable: true, + writable: true, + }); + }; +} + +describe("github publish source helpers", () => { + it.each([ + [ + "owner/repo", + { + kind: "github", + owner: "owner", + repo: "repo", + path: ".", + url: "https://github.com/owner/repo", + }, + ], + [ + "owner/repo@v1.0.0", + { + kind: "github", + owner: "owner", + repo: "repo", + ref: "v1.0.0", + path: ".", + url: "https://github.com/owner/repo", + }, + ], + [ + "owner/repo@main", + { + kind: "github", + owner: "owner", + repo: "repo", + ref: "main", + path: ".", + url: "https://github.com/owner/repo", + }, + ], + [ + "https://github.com/owner/repo", + { + kind: "github", + owner: "owner", + repo: "repo", + path: ".", + url: "https://github.com/owner/repo", + }, + ], + [ + "https://github.com/owner/repo/tree/main", + { + kind: "github", + owner: "owner", + repo: "repo", + ref: "main", + path: ".", + url: "https://github.com/owner/repo", + }, + ], + [ + "https://github.com/owner/repo/tree/main/plugins/demo", + { + kind: "github", + owner: "owner", + repo: "repo", + ref: "main", + path: "plugins/demo", + url: "https://github.com/owner/repo", + }, + ], + [ + "https://github.com/owner/repo/blob/main/plugins/demo/index.ts", + { + kind: "github", + owner: "owner", + repo: "repo", + ref: "main", + path: "plugins/demo", + url: "https://github.com/owner/repo", + }, + ], + [ + "https://github.com/owner/repo.git", + { + kind: "github", + owner: "owner", + repo: "repo", + path: ".", + url: "https://github.com/owner/repo", + }, + ], + ])("parses %s as a GitHub source", async (input, expected) => { + const workdir = await makeTmpDir(); + const restoreFetch = + input.includes("/tree/") || input.includes("/blob/") + ? mockGitHubCommitLookup([(expected as { ref?: string }).ref ?? ""]) + : null; + try { + await expect(resolveSourceInput(input, { workdir })).resolves.toEqual(expected); + } finally { + restoreFetch?.(); + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("parses tree URLs whose refs contain slashes", async () => { + const workdir = await makeTmpDir(); + const restoreFetch = mockGitHubCommitLookup(["feature/new-ui"]); + try { + await expect( + resolveSourceInput("https://github.com/owner/repo/tree/feature/new-ui/plugins/demo", { + workdir, + }), + ).resolves.toEqual({ + kind: "github", + owner: "owner", + repo: "repo", + ref: "feature/new-ui", + path: "plugins/demo", + url: "https://github.com/owner/repo", + }); + } finally { + restoreFetch(); + await rm(workdir, { recursive: true, force: true }); + } + }); + + it.each([ + "./local-folder", + "/absolute/path", + "~/path", + ".", + "@scope/package", + "owner/repo/extra", + ])("treats %s as a local path", async (input) => { + const workdir = await makeTmpDir(); + try { + const resolved = await resolveSourceInput(input, { workdir }); + expect(resolved.kind).toBe("local"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("prefers an existing local directory over GitHub shorthand", async () => { + const workdir = await makeTmpDir(); + try { + const localDir = join(workdir, "owner", "repo"); + await mkdir(localDir, { recursive: true }); + + await expect(resolveSourceInput("owner/repo", { workdir })).resolves.toEqual({ + kind: "local", + path: localDir, + }); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("prefers an existing local path from an alternate workdir", async () => { + const workspace = await makeTmpDir(); + const callerCwd = await makeTmpDir(); + try { + const localDir = join(callerCwd, "plugin"); + await mkdir(localDir, { recursive: true }); + + await expect( + resolveSourceInput(".", { workdir: workspace, localWorkdirs: [callerCwd, workspace] }), + ).resolves.toEqual({ + kind: "local", + path: callerCwd, + }); + + await expect( + resolveSourceInput("plugin", { workdir: workspace, localWorkdirs: [callerCwd, workspace] }), + ).resolves.toEqual({ + kind: "local", + path: localDir, + }); + } finally { + await rm(callerCwd, { recursive: true, force: true }); + await rm(workspace, { recursive: true, force: true }); + } + }); + + it("resolves git metadata for a nested folder in a real git repo", async () => { + const root = await makeTmpDir(); + try { + const nested = join(root, "plugins", "demo"); + await mkdir(nested, { recursive: true }); + await writeFile(join(nested, "package.json"), '{"name":"demo"}\n', "utf8"); + + runGit(root, ["init", "-b", "main"]); + runGit(root, ["remote", "add", "origin", "git@github.com:openclaw/demo-repo.git"]); + runGit(root, ["add", "."]); + runGit(root, [ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-m", + "init", + ]); + const commit = runGit(root, ["rev-parse", "HEAD"]); + const gitRoot = runGit(root, ["rev-parse", "--show-toplevel"]); + runGit(root, ["-c", "tag.gpgSign=false", "tag", "v1.0.0"]); + + expect(resolveLocalGitInfo(nested)).toEqual({ + root: gitRoot, + path: "plugins/demo", + repo: "openclaw/demo-repo", + commit, + ref: "v1.0.0", + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("returns null for a non-git folder", async () => { + const workdir = await makeTmpDir(); + try { + const folder = join(workdir, "not-a-repo"); + await mkdir(folder, { recursive: true }); + expect(resolveLocalGitInfo(folder)).toBeNull(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("extracts GitHub archives that contain explicit directory entries", async () => { + const archiveBytes = zipSync({ + "repo-root/.agents/": new Uint8Array(), + "repo-root/.agents/config.json": new TextEncoder().encode('{"ok":true}\n'), + "repo-root/package.json": new TextEncoder().encode('{"name":"demo","version":"1.0.0"}\n'), + "repo-root/openclaw.plugin.json": new TextEncoder().encode( + '{"id":"demo","configSchema":{"type":"object"}}\n', + ), + }); + const archiveBody = archiveBytes.buffer.slice( + archiveBytes.byteOffset, + archiveBytes.byteOffset + archiveBytes.byteLength, + ) as ArrayBuffer; + + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify({ default_branch: "main" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response(archiveBody, { + status: 200, + headers: { "content-type": "application/zip" }, + }), + ); + const originalFetch = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { + value: fetchMock, + configurable: true, + writable: true, + }); + + const fetched = await fetchGitHubSource({ + kind: "github", + owner: "owner", + repo: "repo", + path: ".", + url: "https://github.com/owner/repo", + }); + + try { + expect(await readFile(join(fetched.dir, ".agents", "config.json"), "utf8")).toContain( + '"ok":true', + ); + expect(await readFile(join(fetched.dir, "package.json"), "utf8")).toContain('"name":"demo"'); + } finally { + await fetched.cleanup(); + Object.defineProperty(globalThis, "fetch", { + value: originalFetch, + configurable: true, + writable: true, + }); + } + }); + + it("rejects GitHub archives with unsafe paths", async () => { + const archiveBytes = zipSync({ + "repo-root/../../escape.txt": new TextEncoder().encode("bad\n"), + "repo-root/package.json": new TextEncoder().encode('{"name":"demo","version":"1.0.0"}\n'), + "repo-root/openclaw.plugin.json": new TextEncoder().encode( + '{"id":"demo","configSchema":{"type":"object"}}\n', + ), + }); + const archiveBody = archiveBytes.buffer.slice( + archiveBytes.byteOffset, + archiveBytes.byteOffset + archiveBytes.byteLength, + ) as ArrayBuffer; + + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify({ default_branch: "main" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ) + .mockResolvedValueOnce( + new Response(archiveBody, { + status: 200, + headers: { "content-type": "application/zip" }, + }), + ); + const originalFetch = globalThis.fetch; + Object.defineProperty(globalThis, "fetch", { + value: fetchMock, + configurable: true, + writable: true, + }); + + try { + await expect( + fetchGitHubSource({ + kind: "github", + owner: "owner", + repo: "repo", + path: ".", + url: "https://github.com/owner/repo", + }), + ).rejects.toThrow(/Unsafe path in archive/i); + } finally { + Object.defineProperty(globalThis, "fetch", { + value: originalFetch, + configurable: true, + writable: true, + }); + } + }); +}); diff --git a/dt-skill/src/cli/commands/github.ts b/dt-skill/src/cli/commands/github.ts new file mode 100644 index 00000000..a74561b8 --- /dev/null +++ b/dt-skill/src/cli/commands/github.ts @@ -0,0 +1,417 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import { dirname, join, resolve, sep } from "node:path"; +import { unzipSync } from "fflate"; + +const GITHUB_API = "https://api.github.com"; +const GITHUB_HOSTS = new Set(["github.com", "www.github.com"]); +const ZIP_USER_AGENT = "clawhub/package-publish"; + +type ResolvedPublishSource = + | { + kind: "local"; + path: string; + } + | { + kind: "github"; + owner: string; + repo: string; + ref?: string; + path: string; + url: string; + }; + +type LocalGitInfo = { + root: string; + path: string; + repo?: string; + commit?: string; + ref?: string; +}; + +type FetchedGitHubSource = { + dir: string; + source: { + kind: "github"; + url: string; + repo: string; + ref: string; + commit: string; + path: string; + importedAt: number; + }; + cleanup: () => Promise; +}; + +export async function resolveSourceInput( + input: string, + options: { workdir: string; localWorkdirs?: string[] }, +): Promise { + const trimmed = input.trim(); + if (!trimmed) throw new Error("Path required"); + const localWorkdirs = normalizeLocalWorkdirs(options.workdir, options.localWorkdirs); + + if (trimmed.startsWith("https://")) { + return await parseGitHubUrl(trimmed); + } + + const shorthand = parseGitHubShorthand(trimmed); + if (shorthand) { + for (const workdir of localWorkdirs) { + const localPath = resolveLocalPath(workdir, trimmed); + const localStat = await stat(localPath).catch(() => null); + if (localStat?.isDirectory()) { + return { kind: "local", path: localPath }; + } + } + return shorthand; + } + + for (const workdir of localWorkdirs) { + const localPath = resolveLocalPath(workdir, trimmed); + if (await stat(localPath).catch(() => null)) { + return { kind: "local", path: localPath }; + } + } + + return { kind: "local", path: resolveLocalPath(localWorkdirs[0] ?? options.workdir, trimmed) }; +} + +export async function fetchGitHubSource( + source: Extract, +) { + const token = process.env.GITHUB_TOKEN?.trim() || undefined; + const repo = `${source.owner}/${source.repo}`; + const repoUrl = `https://github.com/${repo}`; + const resolvedRef = + source.ref?.trim() || (await resolveDefaultBranch(source.owner, source.repo, token)); + const commit = await resolveCommitSha(source.owner, source.repo, resolvedRef, token); + const archiveBytes = await downloadGitHubZip(source.owner, source.repo, commit, token); + const entries = stripSingleTopLevelFolder(unzipSync(archiveBytes)); + const publishPath = normalizeRepoSubpath(source.path); + const tempDir = await mkdtemp(join(tmpdir(), "clawhub-github-publish-")); + + try { + const subdirEntries = filterEntriesForSubpath(entries, publishPath); + if (Object.keys(subdirEntries).length === 0) { + throw new Error(`GitHub path "${publishPath}" does not contain any files`); + } + await writeEntries(tempDir, subdirEntries); + } catch (error) { + await rm(tempDir, { recursive: true, force: true }); + throw error; + } + + return { + dir: tempDir, + source: { + kind: "github" as const, + url: repoUrl, + repo, + ref: resolvedRef, + commit, + path: publishPath, + importedAt: Date.now(), + }, + cleanup: async () => { + await rm(tempDir, { recursive: true, force: true }); + }, + } satisfies FetchedGitHubSource; +} + +export function resolveLocalGitInfo(folder: string): LocalGitInfo | null { + const root = runGit(folder, ["rev-parse", "--show-toplevel"]); + if (!root) return null; + + const prefix = runGit(folder, ["rev-parse", "--show-prefix"]); + const commit = runGit(folder, ["rev-parse", "HEAD"]) || undefined; + const ref = + runGit(folder, ["describe", "--tags", "--exact-match"]) || + runGit(folder, ["branch", "--show-current"]) || + commit; + const repo = normalizeGitHubRepo(runGit(folder, ["remote", "get-url", "origin"]) || ""); + + return { + root: root, + path: normalizePath(prefix || "") || ".", + repo: repo || undefined, + commit, + ref: ref || undefined, + }; +} + +export function normalizeGitHubRepo(value: string) { + const trimmed = value + .trim() + .replace(/^git\+/, "") + .replace(/\.git$/i, "") + .replace(/^git@github\.com:/i, "https://github.com/"); + if (!trimmed) return undefined; + + const shorthand = trimmed.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i); + if (shorthand) return `${shorthand[1]}/${shorthand[2]}`; + + try { + const url = new URL(trimmed); + if (!GITHUB_HOSTS.has(url.hostname)) return undefined; + const segments = decodePathSegments(url.pathname); + const owner = segments[0] ?? ""; + const repo = (segments[1] ?? "").replace(/\.git$/i, ""); + if (!owner || !repo) return undefined; + return `${owner}/${repo}`; + } catch { + return undefined; + } +} + +function parseGitHubShorthand( + input: string, +): Extract | null { + const atIndex = input.lastIndexOf("@"); + const rawRepo = atIndex > 0 ? input.slice(0, atIndex) : input; + const rawRef = atIndex > 0 ? input.slice(atIndex + 1).trim() : ""; + if ( + !rawRepo || + rawRepo.startsWith(".") || + rawRepo.startsWith("~") || + rawRepo.startsWith("/") || + rawRepo.includes("\\") + ) { + return null; + } + const match = rawRepo.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i); + if (!match) return null; + + return { + kind: "github", + owner: match[1], + repo: match[2], + ...(rawRef ? { ref: rawRef } : {}), + path: ".", + url: `https://github.com/${match[1]}/${match[2]}`, + }; +} + +async function parseGitHubUrl( + input: string, +): Promise> { + let url: URL; + try { + url = new URL(input); + } catch { + throw new Error("Invalid GitHub URL"); + } + if (url.protocol !== "https:") throw new Error("Only https:// GitHub URLs are supported"); + if (!GITHUB_HOSTS.has(url.hostname)) throw new Error("Only github.com URLs are supported"); + + const segments = decodePathSegments(url.pathname); + const owner = segments[0] ?? ""; + const repo = (segments[1] ?? "").replace(/\.git$/i, ""); + if (!owner || !repo) throw new Error("GitHub URL must be //"); + + const kind = segments[2] ?? ""; + if (!kind || (kind !== "tree" && kind !== "blob")) { + return { + kind: "github", + owner, + repo, + path: ".", + url: `https://github.com/${owner}/${repo}`, + }; + } + + const { ref, path } = await resolveGitHubUrlRefAndPath(owner, repo, kind, segments.slice(3)); + + return { + kind: "github", + owner, + repo, + ref, + path, + url: `https://github.com/${owner}/${repo}`, + }; +} + +function normalizeRepoSubpath(value: string) { + const normalized = normalizePath(value.trim()); + if (!normalized || normalized === ".") return "."; + const segments = normalized.split("/"); + if (segments.some((segment) => !segment || segment === "." || segment === "..")) { + throw new Error("Invalid GitHub path"); + } + return segments.join("/"); +} + +function resolveLocalPath(workdir: string, input: string) { + if (input === "~") return homedir(); + if (input.startsWith("~/")) return resolve(homedir(), input.slice(2)); + return resolve(workdir, input); +} + +function normalizeLocalWorkdirs(workdir: string, localWorkdirs?: string[]) { + const values = localWorkdirs?.length ? localWorkdirs : [workdir]; + return Array.from(new Set(values.map((value) => resolve(value)))); +} + +function normalizePath(pathValue: string) { + return pathValue + .split(/[\\/]+/) + .filter(Boolean) + .join("/") + .replace(/^\.\/+/, ""); +} + +function decodePathSegments(pathname: string) { + return pathname + .split("/") + .map((segment) => segment.trim()) + .filter(Boolean) + .map((segment) => { + try { + return decodeURIComponent(segment); + } catch { + throw new Error("Invalid GitHub URL"); + } + }); +} + +async function resolveDefaultBranch(owner: string, repo: string, token?: string) { + const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}`, { + headers: buildGitHubHeaders(token), + }); + if (!response.ok) throw new Error(`GitHub repo not found: ${owner}/${repo}`); + const parsed = (await response.json()) as { default_branch?: unknown }; + const defaultBranch = + typeof parsed.default_branch === "string" ? parsed.default_branch.trim() : ""; + if (!defaultBranch) throw new Error("GitHub repo default branch missing"); + return defaultBranch; +} + +async function resolveCommitSha(owner: string, repo: string, ref: string, token?: string) { + const response = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, + { + headers: buildGitHubHeaders(token), + }, + ); + if (!response.ok) throw new Error(`GitHub ref not found: ${owner}/${repo}@${ref}`); + const parsed = (await response.json()) as { sha?: unknown }; + const sha = typeof parsed.sha === "string" ? parsed.sha.trim().toLowerCase() : ""; + if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error("GitHub commit sha missing"); + return sha; +} + +async function tryResolveCommitSha(owner: string, repo: string, ref: string, token?: string) { + const response = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, + { + headers: buildGitHubHeaders(token), + }, + ); + if (!response.ok) return null; + const parsed = (await response.json()) as { sha?: unknown }; + const sha = typeof parsed.sha === "string" ? parsed.sha.trim().toLowerCase() : ""; + return /^[a-f0-9]{40}$/.test(sha) ? sha : null; +} + +async function downloadGitHubZip(owner: string, repo: string, ref: string, token?: string) { + const response = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/zipball/${encodeURIComponent(ref)}`, + { + headers: buildGitHubHeaders(token), + }, + ); + if (!response.ok) throw new Error(`GitHub archive download failed: ${owner}/${repo}@${ref}`); + return new Uint8Array(await response.arrayBuffer()); +} + +function buildGitHubHeaders(token?: string) { + const headers: Record = { + Accept: "application/vnd.github+json", + "User-Agent": ZIP_USER_AGENT, + }; + if (token) headers.Authorization = `Bearer ${token}`; + return headers; +} + +function stripSingleTopLevelFolder(entries: Record) { + const paths = Object.keys(entries); + if (paths.length === 0) return {}; + const firstRoot = paths[0]?.split("/")[0] ?? ""; + if (!firstRoot) return entries; + const prefix = `${firstRoot}/`; + if (!paths.every((path) => path.startsWith(prefix))) return entries; + + const stripped: Record = {}; + for (const [path, bytes] of Object.entries(entries)) { + const next = path.slice(prefix.length); + if (!next) continue; + stripped[next] = bytes; + } + return stripped; +} + +function filterEntriesForSubpath(entries: Record, subpath: string) { + if (subpath === ".") return entries; + const prefix = `${subpath}/`; + const filtered: Record = {}; + for (const [path, bytes] of Object.entries(entries)) { + if (!path.startsWith(prefix)) continue; + const relPath = path.slice(prefix.length); + if (!relPath) continue; + filtered[relPath] = bytes; + } + return filtered; +} + +async function writeEntries(root: string, entries: Record) { + const absRoot = resolve(root); + for (const [path, bytes] of Object.entries(entries)) { + if (!path || path.endsWith("/")) continue; + const absPath = resolve(absRoot, ...path.split("/")); + if (absPath !== absRoot && !absPath.startsWith(`${absRoot}${sep}`)) { + throw new Error(`Unsafe path in archive: ${path}`); + } + await mkdir(dirname(absPath), { recursive: true }); + await writeFile(absPath, Buffer.from(bytes)); + } +} + +async function resolveGitHubUrlRefAndPath( + owner: string, + repo: string, + kind: "tree" | "blob", + segments: string[], +) { + if (segments.length === 0) throw new Error("Missing ref in GitHub URL"); + + const token = process.env.GITHUB_TOKEN?.trim() || undefined; + const minPathSegments = kind === "blob" ? 1 : 0; + const maxRefSegments = segments.length - minPathSegments; + + for (let refSegmentCount = maxRefSegments; refSegmentCount >= 1; refSegmentCount -= 1) { + const ref = segments.slice(0, refSegmentCount).join("/"); + const pathRemainder = segments.slice(refSegmentCount).join("/"); + if (kind === "blob" && !pathRemainder) continue; + const commit = await tryResolveCommitSha(owner, repo, ref, token); + if (!commit) continue; + const path = + kind === "blob" + ? normalizeRepoSubpath(pathRemainder.split("/").slice(0, -1).join("/") || ".") + : normalizeRepoSubpath(pathRemainder || "."); + return { ref, path }; + } + + throw new Error("GitHub ref not found in URL"); +} + +function runGit(cwd: string, args: string[]) { + const result = spawnSync("git", ["-C", cwd, ...args], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) return null; + const value = result.stdout.trim(); + return value || null; +} diff --git a/dt-skill/src/cli/commands/inspect.test.ts b/dt-skill/src/cli/commands/inspect.test.ts new file mode 100644 index 00000000..f5a7a946 --- /dev/null +++ b/dt-skill/src/cli/commands/inspect.test.ts @@ -0,0 +1,221 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; +import { ApiRoutes } from "../../schema/index.js"; +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); + +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../ui.js", () => uiMocks.moduleFactory()); + +const { cmdInspect } = await import("./inspect"); + +const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); +const mockWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + +afterEach(() => { + vi.clearAllMocks(); + mockLog.mockClear(); + mockWrite.mockClear(); +}); + +describe("cmdInspect", () => { + it("fetches latest version files when --files is set", async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: { latest: "1.2.3" }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { version: "1.2.3", createdAt: 3, changelog: "init", license: "MIT-0" }, + owner: null, + }) + .mockResolvedValueOnce({ + skill: { slug: "demo", displayName: "Demo" }, + version: { version: "1.2.3", createdAt: 3, changelog: "init", files: [] }, + }); + + await cmdInspect(makeGlobalOpts(), "demo", { files: true }); + + const firstArgs = httpMocks.apiRequest.mock.calls[0]?.[1]; + const secondArgs = httpMocks.apiRequest.mock.calls[1]?.[1]; + expect(firstArgs?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent("demo")}`); + expect(secondArgs?.path).toBe( + `${ApiRoutes.skills}/${encodeURIComponent("demo")}/versions/${encodeURIComponent("1.2.3")}`, + ); + }); + + it("uses tag param when fetching a file", async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: { latest: "2.0.0" }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { version: "2.0.0", createdAt: 3, changelog: "init", license: "MIT-0" }, + owner: null, + }) + .mockResolvedValueOnce({ + skill: { slug: "demo", displayName: "Demo" }, + version: { version: "2.0.0", createdAt: 3, changelog: "init", files: [] }, + }); + httpMocks.fetchText.mockResolvedValue("content"); + + await cmdInspect(makeGlobalOpts(), "demo", { file: "SKILL.md", tag: "latest" }); + + const fetchArgs = httpMocks.fetchText.mock.calls[0]?.[1]; + const url = new URL(String(fetchArgs?.url)); + expect(url.pathname).toBe("/api/v1/skills/demo/file"); + expect(url.searchParams.get("path")).toBe("SKILL.md"); + expect(url.searchParams.get("tag")).toBe("latest"); + expect(url.searchParams.get("version")).toBeNull(); + }); + + it("prints security summary when version security metadata exists", async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: { latest: "2.0.0" }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { version: "2.0.0", createdAt: 3, changelog: "init", license: "MIT-0" }, + owner: null, + }) + .mockResolvedValueOnce({ + skill: { slug: "demo", displayName: "Demo" }, + version: { + version: "2.0.0", + createdAt: 3, + changelog: "init", + files: [], + security: { + status: "suspicious", + hasWarnings: true, + checkedAt: 1_700_000_000_000, + model: "gpt-5.2", + }, + }, + }); + + await cmdInspect(makeGlobalOpts(), "demo", { version: "2.0.0" }); + + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("License: MIT-0")); + expect(mockLog).toHaveBeenCalledWith("Security: SUSPICIOUS"); + expect(mockLog).toHaveBeenCalledWith("Warnings: yes"); + expect(mockLog).toHaveBeenCalledWith("Checked: 2023-11-14T22:13:20.000Z"); + expect(mockLog).toHaveBeenCalledWith("Model: gpt-5.2"); + }); + + it("prints skill moderation status without requiring a version fetch", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: { latest: "2.0.0" }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { version: "2.0.0", createdAt: 3, changelog: "init", license: "MIT-0" }, + owner: null, + moderation: { + isSuspicious: true, + isMalwareBlocked: false, + verdict: "suspicious", + reasonCodes: ["network-send", "credential-pattern"], + updatedAt: 1_700_000_000_000, + engineVersion: "scanner-v2", + summary: "Found credential-like configuration and outbound network behavior.", + }, + }); + + await cmdInspect(makeGlobalOpts(), "demo"); + + expect(httpMocks.apiRequest).toHaveBeenCalledTimes(1); + expect(mockLog).toHaveBeenCalledWith("Moderation: SUSPICIOUS"); + expect(mockLog).toHaveBeenCalledWith("Reasons: network-send, credential-pattern"); + expect(mockLog).toHaveBeenCalledWith("Moderation Updated: 2023-11-14T22:13:20.000Z"); + expect(mockLog).toHaveBeenCalledWith("Moderation Engine: scanner-v2"); + expect(mockLog).toHaveBeenCalledWith( + "Moderation Summary: Found credential-like configuration and outbound network behavior.", + ); + }); + + it("does not fall back to an authenticated moderation endpoint", async () => { + httpMocks.apiRequest.mockRejectedValueOnce(new Error("Skill is hidden by quality checks.")); + + await expect(cmdInspect(makeGlobalOpts(), "demo")).rejects.toThrow( + "Skill is hidden by quality checks.", + ); + + expect(httpMocks.apiRequest).toHaveBeenCalledTimes(1); + }); + + it("includes moderation metadata in inspect JSON output", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: {}, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: null, + owner: null, + moderation: { + isSuspicious: false, + isMalwareBlocked: false, + verdict: "clean", + reasonCodes: [], + updatedAt: null, + engineVersion: null, + summary: null, + }, + }); + + await cmdInspect(makeGlobalOpts(), "demo", { json: true }); + + const output = JSON.parse(String(mockLog.mock.calls[0]?.[0])); + expect(output.moderation).toEqual({ + isSuspicious: false, + isMalwareBlocked: false, + verdict: "clean", + reasonCodes: [], + updatedAt: null, + engineVersion: null, + summary: null, + }); + }); + + it("rejects when both version and tag are provided", async () => { + await expect( + cmdInspect(makeGlobalOpts(), "demo", { version: "1.0.0", tag: "latest" }), + ).rejects.toThrow("Use either --version or --tag"); + }); +}); diff --git a/dt-skill/src/cli/commands/inspect.ts b/dt-skill/src/cli/commands/inspect.ts new file mode 100644 index 00000000..4274c87a --- /dev/null +++ b/dt-skill/src/cli/commands/inspect.ts @@ -0,0 +1,445 @@ +import { apiRequest, fetchText, registryUrl } from "../../http.js"; +import { + ApiRoutes, + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_SUMMARY, + ApiV1SkillResponseSchema, + ApiV1SkillVersionListResponseSchema, + ApiV1SkillVersionResponseSchema, +} from "../../schema/index.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError } from "../ui.js"; + +type InspectOptions = { + version?: string; + tag?: string; + versions?: boolean; + limit?: number; + files?: boolean; + file?: string; + json?: boolean; +}; + +type FileEntry = { + path: string; + size: number | null; + sha256: string | null; + contentType: string | null; +}; + +type SecurityStatus = { + status: "clean" | "suspicious" | "malicious" | "pending" | "error"; + hasWarnings: boolean; + checkedAt: number | null; + model: string | null; +}; + +type ModerationStatus = { + isSuspicious: boolean; + isMalwareBlocked: boolean; + verdict?: "clean" | "suspicious" | "malicious"; + reasonCodes?: string[]; + updatedAt?: number | null; + engineVersion?: string | null; + summary?: string | null; + legacyReason?: string | null; +}; + +export async function cmdInspect(opts: GlobalOpts, slug: string, options: InspectOptions = {}) { + const trimmed = slug.trim(); + if (!trimmed) fail("Slug required"); + if (options.version && options.tag) fail("Use either --version or --tag"); + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Fetching skill"); + try { + let skillResult: Awaited> | null = null; + try { + skillResult = await fetchSkillDetail(registry, trimmed); + } catch (error) { + throw error; + } + + if (!skillResult.skill) { + spinner.fail("Skill not found"); + return; + } + + const skill = skillResult.skill; + const tags = normalizeTags(skill.tags); + const latestVersion = skillResult.latestVersion?.version ?? tags.latest ?? null; + const taggedVersion = options.tag ? (tags[options.tag] ?? null) : null; + if (options.tag && !taggedVersion) { + spinner.fail(`Unknown tag "${options.tag}"`); + return; + } + const requestedVersion = options.version ?? taggedVersion ?? null; + + let versionResult: { version: unknown; skill: unknown } | null = null; + if (options.files || options.file || options.version || options.tag) { + const targetVersion = requestedVersion ?? latestVersion; + if (!targetVersion) fail("Could not resolve latest version"); + spinner.text = `Fetching ${trimmed}@${targetVersion}`; + versionResult = await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions/${encodeURIComponent( + targetVersion, + )}`, + }, + ApiV1SkillVersionResponseSchema, + ); + } + + let versionsList: { items?: unknown[]; nextCursor?: string | null } | null = null; + if (options.versions) { + const limit = clampLimit(options.limit ?? 25, 25); + const url = registryUrl( + `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions`, + registry, + ); + url.searchParams.set("limit", String(limit)); + spinner.text = `Fetching versions (${limit})`; + versionsList = await apiRequest( + registry, + { method: "GET", url: url.toString() }, + ApiV1SkillVersionListResponseSchema, + ); + } + + let fileContent: string | null = null; + if (options.file) { + const url = registryUrl(`${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/file`, registry); + url.searchParams.set("path", options.file); + if (options.version) { + url.searchParams.set("version", options.version); + } else if (options.tag) { + url.searchParams.set("tag", options.tag); + } else if (latestVersion) { + url.searchParams.set("version", latestVersion); + } + spinner.text = `Fetching ${options.file}`; + fileContent = await fetchText(registry, { url: url.toString() }); + } + + spinner.stop(); + + const output = { + skill: skillResult.skill, + latestVersion: skillResult.latestVersion, + owner: skillResult.owner, + moderation: skillResult.moderation ?? null, + version: versionResult?.version ?? null, + versions: versionsList?.items ?? null, + file: options.file ? { path: options.file, content: fileContent } : null, + }; + + if (options.json) { + console.log(JSON.stringify(output, null, 2)); + return; + } + + const shouldPrintMeta = !options.file || options.files || options.versions || options.version; + if (shouldPrintMeta) { + printSkillSummary({ + skill, + latestVersion: skillResult.latestVersion, + versionLicense: + (versionResult?.version as { license?: string | null } | undefined)?.license ?? null, + owner: skillResult.owner, + }); + printModerationSummary(skillResult.moderation ?? null); + } + + if (shouldPrintMeta && versionResult?.version) { + printVersionSummary(versionResult.version); + printSecuritySummary(versionResult.version); + } + + if (versionsList?.items && Array.isArray(versionsList.items)) { + if (versionsList.items.length === 0) { + console.log("No versions found."); + } else { + console.log("Versions:"); + for (const item of versionsList.items) { + console.log(formatVersionLine(item)); + } + } + } + + if (versionResult?.version) { + const files = normalizeFiles((versionResult.version as { files?: unknown }).files); + if (options.files) { + if (files.length === 0) { + console.log("No files found."); + } else { + console.log("Files:"); + for (const file of files) { + console.log(formatFileLine(file)); + } + } + } + } + + if (options.file && fileContent !== null) { + if (shouldPrintMeta) console.log(`\n${options.file}:\n`); + process.stdout.write(fileContent); + if (!fileContent.endsWith("\n")) process.stdout.write("\n"); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +function fetchSkillDetail(registry: string, slug: string) { + return apiRequest( + registry, + { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}` }, + ApiV1SkillResponseSchema, + ); +} + +function printSkillSummary(result: { + skill: { + slug: string; + displayName: string; + summary?: string | null; + tags?: unknown; + stats?: unknown; + createdAt: number; + updatedAt: number; + }; + latestVersion?: { + version: string; + createdAt: number; + changelog: string; + license?: string | null; + } | null; + versionLicense?: string | null; + owner?: { handle?: string | null; displayName?: string | null; image?: string | null } | null; +}) { + const { skill } = result; + console.log(`${skill.slug} ${skill.displayName}`); + if (skill.summary) console.log(`Summary: ${skill.summary}`); + const owner = result.owner?.handle || result.owner?.displayName; + if (owner) console.log(`Owner: ${owner}`); + console.log(`Created: ${formatTimestamp(skill.createdAt)}`); + console.log(`Updated: ${formatTimestamp(skill.updatedAt)}`); + if (result.latestVersion?.version) { + console.log(`Latest: ${result.latestVersion.version}`); + } + console.log( + `License: ${result.versionLicense ?? result.latestVersion?.license ?? PLATFORM_SKILL_LICENSE} (${PLATFORM_SKILL_LICENSE_SUMMARY})`, + ); + const tags = normalizeTags(skill.tags); + const tagEntries = Object.entries(tags); + if (tagEntries.length > 0) { + console.log(`Tags: ${tagEntries.map(([tag, version]) => `${tag}=${version}`).join(", ")}`); + } +} + +function printVersionSummary(version: unknown) { + if (!version || typeof version !== "object") return; + const entry = version as { version?: unknown; createdAt?: unknown; changelog?: unknown }; + const value = typeof entry.version === "string" ? entry.version : null; + if (!value) return; + console.log(`Selected: ${value}`); + if (typeof entry.createdAt === "number") { + console.log(`Selected At: ${formatTimestamp(entry.createdAt)}`); + } + if (typeof entry.changelog === "string" && entry.changelog.trim()) { + console.log(`Changelog: ${truncate(entry.changelog, 120)}`); + } +} + +function printModerationSummary(moderation: unknown) { + const status = normalizeModeration(moderation); + if (!status) return; + const label = status.isMalwareBlocked + ? "MALICIOUS" + : status.isSuspicious + ? "SUSPICIOUS" + : (status.verdict ?? "clean").toUpperCase(); + console.log(`Moderation: ${label}`); + if (status.reasonCodes?.length) { + console.log(`Reasons: ${status.reasonCodes.join(", ")}`); + } + if (status.legacyReason) { + console.log(`Moderation Reason: ${status.legacyReason}`); + } + if (typeof status.updatedAt === "number") { + console.log(`Moderation Updated: ${formatTimestamp(status.updatedAt)}`); + } + if (status.engineVersion) { + console.log(`Moderation Engine: ${status.engineVersion}`); + } + if (status.summary) { + console.log(`Moderation Summary: ${truncate(status.summary, 160)}`); + } + if (status.legacyReason === "quality.low") { + console.log( + "Visibility Guidance: publish a substantive update that passes quality assessment, then re-run inspect.", + ); + } +} + +function normalizeModeration(moderation: unknown): ModerationStatus | null { + if (!moderation || typeof moderation !== "object") return null; + const value = moderation as { + isSuspicious?: unknown; + isMalwareBlocked?: unknown; + verdict?: unknown; + reasonCodes?: unknown; + updatedAt?: unknown; + engineVersion?: unknown; + summary?: unknown; + legacyReason?: unknown; + }; + if (typeof value.isSuspicious !== "boolean") return null; + if (typeof value.isMalwareBlocked !== "boolean") return null; + const verdict = + value.verdict === "clean" || value.verdict === "suspicious" || value.verdict === "malicious" + ? value.verdict + : undefined; + const reasonCodes = Array.isArray(value.reasonCodes) + ? value.reasonCodes.filter((reason): reason is string => typeof reason === "string") + : undefined; + return { + isSuspicious: value.isSuspicious, + isMalwareBlocked: value.isMalwareBlocked, + verdict, + reasonCodes, + updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : null, + engineVersion: typeof value.engineVersion === "string" ? value.engineVersion : null, + summary: typeof value.summary === "string" && value.summary.trim() ? value.summary : null, + legacyReason: typeof value.legacyReason === "string" ? value.legacyReason : null, + }; +} + +function normalizeTags(tags: unknown): Record { + if (!tags || typeof tags !== "object") return {}; + const entries = Object.entries(tags as Record); + const resolved: Record = {}; + for (const [tag, version] of entries) { + if (typeof version === "string") resolved[tag] = version; + } + return resolved; +} + +function normalizeFiles(files: unknown): FileEntry[] { + if (!Array.isArray(files)) return []; + return files + .map((file) => { + if (!file || typeof file !== "object") return null; + const entry = file as { + path?: unknown; + size?: unknown; + sha256?: unknown; + contentType?: unknown; + }; + if (typeof entry.path !== "string") return null; + const size = typeof entry.size === "number" ? entry.size : Number(entry.size); + const sha256 = typeof entry.sha256 === "string" ? entry.sha256 : null; + const contentType = typeof entry.contentType === "string" ? entry.contentType : null; + return { + path: entry.path, + size: Number.isFinite(size) ? size : null, + sha256, + contentType, + }; + }) + .filter((entry): entry is FileEntry => Boolean(entry)); +} + +function formatVersionLine(item: unknown) { + if (!item || typeof item !== "object") return "-"; + const entry = item as { version?: unknown; createdAt?: unknown; changelog?: unknown }; + const version = typeof entry.version === "string" ? entry.version : "?"; + const createdAt = + typeof entry.createdAt === "number" ? formatTimestamp(entry.createdAt) : "unknown"; + const changelog = typeof entry.changelog === "string" ? entry.changelog : ""; + const snippet = changelog ? ` ${truncate(changelog, 80)}` : ""; + return `${version} ${createdAt}${snippet}`; +} + +function printSecuritySummary(version: unknown) { + if (!version || typeof version !== "object") return; + const sec = normalizeSecurity((version as { security?: unknown }).security); + if (!sec) return; + console.log(`Security: ${sec.status.toUpperCase()}`); + if (sec.hasWarnings) { + console.log("Warnings: yes"); + } + if (typeof sec.checkedAt === "number") { + console.log(`Checked: ${formatTimestamp(sec.checkedAt)}`); + } + if (sec.model) { + console.log(`Model: ${sec.model}`); + } +} + +function normalizeSecurity(security: unknown): SecurityStatus | null { + if (!security || typeof security !== "object") return null; + const value = security as { + status?: unknown; + hasWarnings?: unknown; + checkedAt?: unknown; + model?: unknown; + }; + if ( + value.status !== "clean" && + value.status !== "suspicious" && + value.status !== "malicious" && + value.status !== "pending" && + value.status !== "error" + ) { + return null; + } + if (typeof value.hasWarnings !== "boolean") return null; + const checkedAt = typeof value.checkedAt === "number" ? value.checkedAt : null; + const model = typeof value.model === "string" ? value.model : null; + return { + status: value.status, + hasWarnings: value.hasWarnings, + checkedAt, + model, + }; +} + +function formatFileLine(file: FileEntry) { + const size = file.size === null ? "?" : formatBytes(file.size); + const sha = file.sha256 ?? "?"; + const type = file.contentType ? ` ${file.contentType}` : ""; + return `${file.path} ${size} ${sha}${type}`; +} + +function formatTimestamp(timestamp: number) { + if (!Number.isFinite(timestamp)) return "unknown"; + return new Date(timestamp).toISOString(); +} + +function formatBytes(bytes: number) { + if (!Number.isFinite(bytes)) return "?"; + const units = ["B", "KB", "MB", "GB"]; + let value = bytes; + let index = 0; + while (value >= 1024 && index < units.length - 1) { + value /= 1024; + index += 1; + } + const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10; + return `${rounded}${units[index]}`; +} + +function clampLimit(limit: number, fallback: number) { + if (!Number.isFinite(limit)) return fallback; + return Math.min(Math.max(1, Math.round(limit)), 200); +} + +function truncate(str: string, maxLen: number) { + if (str.length <= maxLen) return str; + return `${str.slice(0, maxLen - 3)}...`; +} diff --git a/dt-skill/src/cli/commands/moderationPlan.test.ts b/dt-skill/src/cli/commands/moderationPlan.test.ts new file mode 100644 index 00000000..433a892d --- /dev/null +++ b/dt-skill/src/cli/commands/moderationPlan.test.ts @@ -0,0 +1,41 @@ +/* @vitest-environment node */ + +import { describe, expect, it } from "vitest"; +import { reportModerationPlan } from "./moderationPlan"; + +describe("moderation plan summaries", () => { + it.each([ + { + name: "confirmed skill report with hide", + plan: reportModerationPlan({ + entityLabel: "skill", + reportId: "skillReports:1", + status: "confirmed", + finalAction: "hide", + }), + expected: { + subject: "skill report skillReports:1", + outcome: "set status to confirmed; final action hide", + impacts: ["Mark the report as confirmed.", "Hide the skill from public availability."], + requiresConfirmation: true, + }, + }, + { + name: "dismissed package report with no final action", + plan: reportModerationPlan({ + entityLabel: "package", + reportId: "packageReports:1", + status: "dismissed", + finalAction: "none", + }), + expected: { + subject: "package report packageReports:1", + outcome: "set status to dismissed; final action none", + impacts: ["Dismiss the report without changing artifact availability."], + requiresConfirmation: false, + }, + }, + ])("describes $name", ({ plan, expected }) => { + expect(plan).toMatchObject(expected); + }); +}); diff --git a/dt-skill/src/cli/commands/moderationPlan.ts b/dt-skill/src/cli/commands/moderationPlan.ts new file mode 100644 index 00000000..fbeba562 --- /dev/null +++ b/dt-skill/src/cli/commands/moderationPlan.ts @@ -0,0 +1,64 @@ +import { fail, isInteractive, promptConfirm } from "../ui.js"; + +type ModerationPlanOptions = { + json?: boolean; + yes?: boolean; +}; + +type ModerationPlan = { + subject: string; + outcome: string; + impacts: string[]; + requiresConfirmation: boolean; + confirmPrompt: string; +}; + +export function reportModerationPlan(params: { + entityLabel: "skill" | "package"; + reportId: string; + status: "open" | "confirmed" | "dismissed"; + finalAction?: "none" | "hide" | "quarantine" | "revoke"; +}): ModerationPlan { + const impacts: string[] = []; + if (params.status === "open") { + impacts.push("Reopen the report for review."); + } else if (params.status === "confirmed") { + impacts.push("Mark the report as confirmed."); + } else { + impacts.push("Dismiss the report without changing artifact availability."); + } + + if (params.finalAction === "hide") { + impacts.push("Hide the skill from public availability."); + } else if (params.finalAction === "quarantine") { + impacts.push("Quarantine the package release."); + } else if (params.finalAction === "revoke") { + impacts.push("Revoke the package release."); + } + + const action = params.finalAction && params.finalAction !== "none" ? params.finalAction : "none"; + return { + subject: `${params.entityLabel} report ${params.reportId}`, + outcome: `set status to ${params.status}; final action ${action}`, + impacts, + requiresConfirmation: action !== "none", + confirmPrompt: `Apply this ${params.entityLabel} report action?`, + }; +} + +export async function presentModerationPlan(plan: ModerationPlan, options: ModerationPlanOptions) { + if (!options.json) { + console.log("Moderation action summary"); + console.log(` case: ${plan.subject}`); + console.log(` outcome: ${plan.outcome}`); + console.log(" public impact:"); + for (const impact of plan.impacts) { + console.log(` - ${impact}`); + } + } + + if (!plan.requiresConfirmation || options.yes) return; + if (!isInteractive()) fail("Pass --yes (no input)"); + const confirmed = await promptConfirm(plan.confirmPrompt); + if (!confirmed) fail("Canceled"); +} diff --git a/dt-skill/src/cli/commands/ownership.test.ts b/dt-skill/src/cli/commands/ownership.test.ts new file mode 100644 index 00000000..85b7f20b --- /dev/null +++ b/dt-skill/src/cli/commands/ownership.test.ts @@ -0,0 +1,76 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); + +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../ui.js", () => uiMocks.moduleFactory()); + +const { cmdMergeSkill, cmdRenameSkill } = await import("./ownership"); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("ownership commands", () => { + it("rename requires --yes when input is disabled", async () => { + await expect(cmdRenameSkill(makeGlobalOpts(), "demo", "demo-new", {}, false)).rejects.toThrow( + /--yes/i, + ); + }); + + it("rename calls rename endpoint", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + slug: "demo-new", + previousSlug: "demo", + }); + + await cmdRenameSkill(makeGlobalOpts(), "Demo", "Demo-New", { yes: true }, false); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "POST", + path: "/api/v1/skills/demo/rename", + }), + expect.anything(), + ); + const requestArgs = httpMocks.apiRequest.mock.calls[0]?.[1] as { body?: unknown }; + expect(requestArgs.body).toEqual({ newSlug: "demo-new" }); + }); + + it("merge calls merge endpoint", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + sourceSlug: "demo-old", + targetSlug: "demo", + }); + + await cmdMergeSkill(makeGlobalOpts(), "Demo-Old", "Demo", { yes: true }, false); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "POST", + path: "/api/v1/skills/demo-old/merge", + }), + expect.anything(), + ); + const requestArgs = httpMocks.apiRequest.mock.calls[0]?.[1] as { body?: unknown }; + expect(requestArgs.body).toEqual({ targetSlug: "demo" }); + }); +}); diff --git a/dt-skill/src/cli/commands/ownership.ts b/dt-skill/src/cli/commands/ownership.ts new file mode 100644 index 00000000..ba6a2df4 --- /dev/null +++ b/dt-skill/src/cli/commands/ownership.ts @@ -0,0 +1,113 @@ +import { apiRequest } from "../../http.js"; +import { + ApiRoutes, + ApiV1SkillMergeResponseSchema, + ApiV1SkillRenameResponseSchema, + parseArk, +} from "../../schema/index.js"; +import { requireAuthToken } from "../authToken.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; + +type ConfirmOptions = { yes?: boolean }; + +function normalizeSlug(slugArg: string, label = "Skill slug") { + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail(`${label} required`); + return slug; +} + +function canPrompt(inputAllowed: boolean) { + return isInteractive() && inputAllowed !== false; +} + +async function requireYesOrConfirm(options: ConfirmOptions, inputAllowed: boolean, prompt: string) { + if (options.yes) return true; + if (!canPrompt(inputAllowed)) fail("Pass --yes (no input)"); + return promptConfirm(prompt); +} + +export async function cmdRenameSkill( + opts: GlobalOpts, + slugArg: string, + newSlugArg: string, + options: ConfirmOptions, + inputAllowed: boolean, +) { + const slug = normalizeSlug(slugArg); + const newSlug = normalizeSlug(newSlugArg, "New slug"); + if (slug === newSlug) fail("New slug must be different"); + + const confirmed = await requireYesOrConfirm( + options, + inputAllowed, + `Rename ${slug} to ${newSlug}? Old slug will redirect.`, + ); + if (!confirmed) return undefined; + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Renaming ${slug} to ${newSlug}`); + + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/rename`, + token, + body: { newSlug }, + }, + ApiV1SkillRenameResponseSchema, + ); + const parsed = parseArk(ApiV1SkillRenameResponseSchema, result, "Rename skill response"); + spinner.succeed(`Renamed ${parsed.previousSlug} to ${parsed.slug}`); + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdMergeSkill( + opts: GlobalOpts, + sourceSlugArg: string, + targetSlugArg: string, + options: ConfirmOptions, + inputAllowed: boolean, +) { + const sourceSlug = normalizeSlug(sourceSlugArg, "Source slug"); + const targetSlug = normalizeSlug(targetSlugArg, "Target slug"); + if (sourceSlug === targetSlug) fail("Target slug must be different"); + + const confirmed = await requireYesOrConfirm( + options, + inputAllowed, + `Merge ${sourceSlug} into ${targetSlug}? Source slug will redirect and stop listing publicly.`, + ); + if (!confirmed) return undefined; + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Merging ${sourceSlug} into ${targetSlug}`); + + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/${encodeURIComponent(sourceSlug)}/merge`, + token, + body: { targetSlug }, + }, + ApiV1SkillMergeResponseSchema, + ); + const parsed = parseArk(ApiV1SkillMergeResponseSchema, result, "Merge skill response"); + spinner.succeed(`Merged ${parsed.sourceSlug} into ${parsed.targetSlug}`); + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} diff --git a/dt-skill/src/cli/commands/packages.test.ts b/dt-skill/src/cli/commands/packages.test.ts new file mode 100644 index 00000000..e3f49549 --- /dev/null +++ b/dt-skill/src/cli/commands/packages.test.ts @@ -0,0 +1,2497 @@ +/* @vitest-environment node */ + +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { gzipSync, zipSync } from "fflate"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; +import { MAX_CLAWSCAN_NOTE_CHARS } from "../../schema/index.js"; + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); +const originalOidcRequestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; +const originalOidcRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../ui.js", () => uiMocks.moduleFactory()); + +const { + cmdDeletePackage, + cmdDownloadPackage, + cmdExplorePackages, + cmdGetPackageTrustedPublisher, + cmdInspectPackage, + cmdPackageModerationStatus, + cmdPackageMigrationStatus, + cmdPackageReadiness, + cmdPackPackage, + cmdPublishPackage, + cmdReportPackage, + cmdTransferPackage, + cmdUndeletePackage, + cmdVerifyPackage, +} = await import("./packages"); +const { + cmdBackfillPackageArtifacts, + cmdDeletePackageTrustedPublisher, + cmdListPackageMigrations, + cmdListPackageReports, + cmdModeratePackageRelease, + cmdPackageModerationQueue, + cmdSetPackageTrustedPublisher, + cmdTriagePackageReport, + cmdUpsertPackageMigration, +} = await import("../../../../clawhub-mod/src/commands/packages"); +const { parseClawPack } = await import("../../clawpack"); + +const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); +const mockWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + +function makeOpts(workdir = "/work") { + return makeGlobalOpts(workdir); +} + +async function makeTmpWorkdir() { + return await mkdtemp(join(tmpdir(), "clawhub-package-")); +} + +function runGit(cwd: string, args: string[]) { + const result = spawnSync("git", ["-C", cwd, ...args], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); + } + return result.stdout.trim(); +} + +function getPublishForm() { + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/v1/packages"; + }); + if (!publishCall) throw new Error("Missing publish call"); + const form = (publishCall[1] as { form?: FormData }).form; + if (!(form instanceof FormData)) throw new Error("Missing publish form"); + return form; +} + +function getPublishPayload() { + const form = getPublishForm(); + const payloadEntry = form.get("payload"); + if (typeof payloadEntry !== "string") throw new Error("Missing publish payload"); + return JSON.parse(payloadEntry) as Record; +} + +function getUploadedFileNames() { + const form = getPublishForm(); + return (form.getAll("files") as Array) + .map((file) => file.name ?? "") + .sort(); +} + +function getUploadedClawPackNames() { + const form = getPublishForm(); + return (form.getAll("clawpack") as Array) + .map((file) => file.name ?? "") + .sort(); +} + +function getUploadedClawPacks() { + const form = getPublishForm(); + return form.getAll("clawpack") as Array; +} + +function makeCodePluginPackageJson(overrides: Record) { + return JSON.stringify({ + openclaw: { + extensions: ["./dist/index.js"], + hostTargets: ["darwin-arm64", "linux-x64", "win32-x64"], + environment: {}, + compat: { + pluginApi: ">=2026.3.24-beta.2", + }, + build: { + openclawVersion: "2026.3.24-beta.2", + }, + }, + ...overrides, + }); +} + +const TAR_BLOCK_SIZE = 512; + +function writeTarString(target: Uint8Array, offset: number, width: number, value: string) { + const encoded = new TextEncoder().encode(value); + target.set(encoded.subarray(0, width), offset); +} + +function tarOctal(value: number, width: number) { + return value.toString(8).padStart(width - 1, "0") + "\0"; +} + +function tarFile(path: string, content: string) { + const bytes = new TextEncoder().encode(content); + const header = new Uint8Array(TAR_BLOCK_SIZE); + writeTarString(header, 0, 100, path); + writeTarString(header, 100, 8, tarOctal(0o644, 8)); + writeTarString(header, 108, 8, tarOctal(0, 8)); + writeTarString(header, 116, 8, tarOctal(0, 8)); + writeTarString(header, 124, 12, tarOctal(bytes.byteLength, 12)); + writeTarString(header, 136, 12, tarOctal(0, 12)); + header.fill(0x20, 148, 156); + header[156] = "0".charCodeAt(0); + writeTarString(header, 257, 6, "ustar"); + writeTarString(header, 263, 2, "00"); + + let checksum = 0; + for (const byte of header) checksum += byte; + writeTarString(header, 148, 8, tarOctal(checksum, 8)); + + const paddedSize = Math.ceil(bytes.byteLength / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; + const body = new Uint8Array(paddedSize); + body.set(bytes); + return [header, body]; +} + +function npmPackFixture(files: Record) { + const parts: Uint8Array[] = []; + for (const [path, content] of Object.entries(files)) { + parts.push(...tarFile(path, content)); + } + parts.push(new Uint8Array(TAR_BLOCK_SIZE), new Uint8Array(TAR_BLOCK_SIZE)); + const size = parts.reduce((sum, part) => sum + part.byteLength, 0); + const tar = new Uint8Array(size); + let offset = 0; + for (const part of parts) { + tar.set(part, offset); + offset += part.byteLength; + } + return gzipSync(tar); +} + +function artifactIdentity(bytes: Uint8Array) { + return { + sha256: createHash("sha256").update(bytes).digest("hex"), + npmIntegrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`, + npmShasum: createHash("sha1").update(bytes).digest("hex"), + }; +} + +afterEach(() => { + vi.clearAllMocks(); + mockLog.mockClear(); + mockWrite.mockClear(); + uiMocks.spinner.text = ""; + vi.unstubAllGlobals(); + if (originalOidcRequestUrl === undefined) { + delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL; + } else { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = originalOidcRequestUrl; + } + if (originalOidcRequestToken === undefined) { + delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + } else { + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = originalOidcRequestToken; + } +}); + +describe("package commands", () => { + it("searches package catalog via /api/v1/packages/search", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + results: [ + { + score: 10, + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + channel: "community", + isOfficial: false, + summary: "Demo plugin", + latestVersion: "1.2.3", + }, + }, + ], + }); + + await cmdExplorePackages(makeOpts(), "demo plugin", { + family: "code-plugin", + executesCode: true, + os: "darwin", + requiresBrowser: true, + externalService: "GitHub", + artifactKind: "npm-pack", + npmMirror: true, + }); + + const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; + const url = new URL(String(request?.url)); + expect(url.pathname).toBe("/api/v1/packages/search"); + expect(url.searchParams.get("q")).toBe("demo plugin"); + expect(url.searchParams.get("family")).toBe("code-plugin"); + expect(url.searchParams.get("executesCode")).toBe("true"); + expect(url.searchParams.get("os")).toBe("darwin"); + expect(url.searchParams.get("requiresBrowser")).toBe("true"); + expect(url.searchParams.get("externalService")).toBe("GitHub"); + expect(url.searchParams.get("artifactKind")).toBe("npm-pack"); + expect(url.searchParams.get("npmMirror")).toBe("true"); + }); + + it("supports skill family package browse requests", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + items: [], + nextCursor: null, + }); + + await cmdExplorePackages(makeOpts(), "", { family: "skill", target: "linux-x64", limit: 7 }); + + const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; + const url = new URL(String(request?.url)); + expect(url.pathname).toBe("/api/v1/packages"); + expect(url.searchParams.get("family")).toBe("skill"); + expect(url.searchParams.get("target")).toBe("linux-x64"); + expect(url.searchParams.get("limit")).toBe("7"); + }); + + it("uses tag param when fetching a package file", async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + package: { + name: "demo", + displayName: "Demo", + family: "code-plugin", + runtimeId: "demo.plugin", + channel: "community", + isOfficial: false, + summary: null, + latestVersion: "2.0.0", + createdAt: 1, + updatedAt: 2, + tags: { latest: "2.0.0" }, + compatibility: null, + capabilities: { executesCode: true }, + verification: { + tier: "structural", + scope: "artifact-only", + }, + }, + owner: null, + }) + .mockResolvedValueOnce({ + package: { name: "demo", displayName: "Demo", family: "code-plugin" }, + version: { + version: "2.0.0", + createdAt: 3, + changelog: "init", + files: [], + }, + }); + httpMocks.fetchText.mockResolvedValue("content"); + + await cmdInspectPackage(makeOpts(), "demo", { file: "README.md", tag: "latest" }); + + const fetchArgs = httpMocks.fetchText.mock.calls[0]?.[1] as { url?: string } | undefined; + const url = new URL(String(fetchArgs?.url)); + expect(url.pathname).toBe("/api/v1/packages/demo/file"); + expect(url.searchParams.get("path")).toBe("README.md"); + expect(url.searchParams.get("tag")).toBe("latest"); + expect(url.searchParams.get("version")).toBeNull(); + }); + + it("downloads a ClawPack artifact through the explicit artifact resolver", async () => { + const workdir = await makeTmpWorkdir(); + try { + const bytes = npmPackFixture({ + "package/package.json": JSON.stringify({ + name: "@scope/demo", + version: "1.2.3", + }), + "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), + }); + const identity = artifactIdentity(bytes); + await mkdir(join(workdir, "downloads"), { recursive: true }); + httpMocks.apiRequest + .mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + runtimeId: "demo.plugin", + channel: "community", + isOfficial: false, + summary: null, + latestVersion: "1.2.3", + createdAt: 1, + updatedAt: 2, + tags: { latest: "1.2.3" }, + }, + owner: null, + }) + .mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + }, + version: "1.2.3", + artifact: { + kind: "npm-pack", + sha256: identity.sha256, + size: bytes.byteLength, + format: "tgz", + npmIntegrity: identity.npmIntegrity, + npmShasum: identity.npmShasum, + npmTarballName: "demo-1.2.3.tgz", + downloadUrl: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", + tarballUrl: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", + legacyDownloadUrl: + "https://clawhub.ai/api/v1/packages/@scope/demo/download?version=1.2.3", + }, + }); + httpMocks.fetchBinary.mockResolvedValue(bytes); + + await cmdDownloadPackage(makeOpts(workdir), "@scope/demo", { + tag: "latest", + output: "downloads", + }); + + expect(httpMocks.apiRequest.mock.calls[1]?.[1]).toMatchObject({ + method: "GET", + path: "/api/v1/packages/%40scope%2Fdemo/versions/1.2.3/artifact", + }); + expect(httpMocks.fetchBinary).toHaveBeenCalledWith("https://clawhub.ai", { + url: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", + token: undefined, + }); + expect(await readFile(join(workdir, "downloads", "demo-1.2.3.tgz"))).toEqual( + Buffer.from(bytes), + ); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Downloaded @scope/demo@1.2.3")); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("downloads legacy ZIP artifacts without enforcing stale stored release digests", async () => { + const workdir = await makeTmpWorkdir(); + try { + const bytes = new TextEncoder().encode("rebuilt legacy zip"); + await mkdir(join(workdir, "downloads"), { recursive: true }); + httpMocks.apiRequest + .mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + runtimeId: "demo.plugin", + channel: "community", + isOfficial: false, + summary: null, + latestVersion: "1.2.3", + createdAt: 1, + updatedAt: 2, + tags: { latest: "1.2.3" }, + }, + owner: null, + }) + .mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + }, + version: "1.2.3", + artifact: { + kind: "legacy-zip", + sha256: "0".repeat(64), + format: "zip", + downloadUrl: "https://clawhub.ai/api/v1/packages/@scope/demo/download?version=1.2.3", + legacyDownloadUrl: + "https://clawhub.ai/api/v1/packages/@scope/demo/download?version=1.2.3", + }, + }); + httpMocks.fetchBinary.mockResolvedValue(bytes); + + await cmdDownloadPackage(makeOpts(workdir), "@scope/demo", { + tag: "latest", + output: "downloads", + }); + + expect(await readFile(join(workdir, "downloads", "scope-demo-1.2.3.zip"))).toEqual( + Buffer.from(bytes), + ); + expect(uiMocks.spinner.fail).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("verifies a local ClawPack against resolved artifact metadata", async () => { + const workdir = await makeTmpWorkdir(); + try { + const bytes = npmPackFixture({ + "package/package.json": JSON.stringify({ + name: "@scope/demo", + version: "1.2.3", + }), + "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), + }); + const identity = artifactIdentity(bytes); + await writeFile(join(workdir, "demo-1.2.3.tgz"), bytes); + httpMocks.apiRequest + .mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + runtimeId: "demo.plugin", + channel: "community", + isOfficial: false, + summary: null, + latestVersion: "1.2.3", + createdAt: 1, + updatedAt: 2, + tags: { latest: "1.2.3" }, + }, + owner: null, + }) + .mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + }, + version: "1.2.3", + artifact: { + kind: "npm-pack", + sha256: identity.sha256, + format: "tgz", + npmIntegrity: identity.npmIntegrity, + npmShasum: identity.npmShasum, + npmTarballName: "demo-1.2.3.tgz", + downloadUrl: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", + }, + }); + + await cmdVerifyPackage(makeOpts(workdir), "demo-1.2.3.tgz", { + packageName: "@scope/demo", + tag: "latest", + }); + + expect(mockLog).toHaveBeenCalledWith("OK. Artifact verification passed."); + expect(uiMocks.spinner.fail).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("fails package artifact verification on digest mismatch", async () => { + const workdir = await makeTmpWorkdir(); + try { + const bytes = npmPackFixture({ + "package/package.json": JSON.stringify({ + name: "@scope/demo", + version: "1.2.3", + }), + }); + await writeFile(join(workdir, "demo-1.2.3.tgz"), bytes); + + await expect( + cmdVerifyPackage(makeOpts(workdir), "demo-1.2.3.tgz", { + sha256: "bad", + }), + ).rejects.toThrow("SHA-256 mismatch"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("sets package release moderation state", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + state: "quarantined", + scanStatus: "malicious", + }); + + await cmdModeratePackageRelease(makeOpts(), "@scope/demo", { + version: "1.2.3", + state: "quarantined", + reason: "suspicious native payload", + }); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/packages/%40scope%2Fdemo/versions/1.2.3/moderation", + token: "tkn", + body: { + state: "quarantined", + reason: "suspicious native payload", + }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith( + "OK. @scope/demo@1.2.3 moderation state set to quarantined.", + ); + }); + + it("reports packages for moderator review", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + reported: true, + alreadyReported: false, + packageId: "pkg_1", + releaseId: "rel_1", + reportCount: 1, + }); + + await cmdReportPackage(makeOpts(), "@scope/demo", { + version: "1.2.3", + reason: "suspicious native payload", + }); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/packages/%40scope%2Fdemo/report", + token: "tkn", + body: { + reason: "suspicious native payload", + version: "1.2.3", + }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("OK. Reported @scope/demo@1.2.3 for moderator review."); + }); + + it("lists package reports", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + items: [ + { + reportId: "packageReports:1", + packageId: "pkg_1", + releaseId: "rel_1", + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + version: "1.2.3", + reason: "suspicious", + status: "open", + createdAt: 1, + reporter: { userId: "users:reporter", handle: "reporter", displayName: "Reporter" }, + triagedAt: null, + triagedBy: null, + triageNote: null, + }, + ], + nextCursor: null, + done: true, + }); + + await cmdListPackageReports(makeOpts(), { status: "open", limit: 10 }); + + const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; + const url = new URL(String(request?.url)); + expect(url.pathname).toBe("/api/v1/packages/reports"); + expect(url.searchParams.get("status")).toBe("open"); + expect(url.searchParams.get("limit")).toBe("10"); + expect(mockLog).toHaveBeenCalledWith("packageReports:1 open @scope/demo@1.2.3"); + }); + + it("triages package reports", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + reportId: "packageReports:1", + packageId: "pkg_1", + status: "confirmed", + reportCount: 0, + actionTaken: "quarantine", + }); + + await cmdTriagePackageReport(makeOpts(), "packageReports:1", { + status: "confirmed", + note: "handled", + action: "quarantine", + yes: true, + }); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/packages/reports/packageReports%3A1/triage", + token: "tkn", + body: { + status: "confirmed", + note: "handled", + finalAction: "quarantine", + }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith( + "OK. Report packageReports:1 set to confirmed; action quarantine.", + ); + expect(mockLog).toHaveBeenCalledWith(" - Quarantine the package release."); + }); + + it("shows package moderation status", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + package: { + packageId: "pkg_1", + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + channel: "community", + isOfficial: false, + reportCount: 2, + lastReportedAt: 456, + scanStatus: "malicious", + }, + latestRelease: { + releaseId: "rel_1", + version: "1.2.3", + artifactKind: "npm-pack", + scanStatus: "malicious", + moderationState: "quarantined", + moderationReason: "manual review", + blockedFromDownload: true, + reasons: ["manual:quarantined", "scan:malicious", "reports:2"], + createdAt: 123, + }, + }); + + await cmdPackageModerationStatus(makeOpts(), "@scope/demo"); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "GET", + path: "/api/v1/packages/%40scope%2Fdemo/moderation", + token: "tkn", + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("@scope/demo moderation"); + expect(mockLog).toHaveBeenCalledWith(" blocked: yes"); + }); + + it("lists the package moderation queue", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + items: [ + { + packageId: "pkg_1", + releaseId: "rel_1", + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + channel: "community", + isOfficial: false, + version: "1.2.3", + createdAt: 1, + artifactKind: "npm-pack", + scanStatus: "malicious", + moderationState: "quarantined", + moderationReason: "manual review", + sourceRepo: "openclaw/demo", + sourceCommit: "abc123", + reportCount: 0, + lastReportedAt: null, + reasons: ["manual:quarantined", "scan:malicious"], + }, + ], + nextCursor: "cursor-1", + done: false, + }); + + await cmdPackageModerationQueue(makeOpts(), { status: "blocked", limit: 10 }); + + const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; + const url = new URL(String(request?.url)); + expect(url.pathname).toBe("/api/v1/packages/moderation/queue"); + expect(url.searchParams.get("status")).toBe("blocked"); + expect(url.searchParams.get("limit")).toBe("10"); + expect(httpMocks.apiRequest.mock.calls[0]?.[1]).toMatchObject({ + method: "GET", + token: "tkn", + }); + expect(mockLog).toHaveBeenCalledWith( + "@scope/demo@1.2.3 malicious quarantined [manual:quarantined, scan:malicious]", + ); + expect(mockLog).toHaveBeenCalledWith("Next cursor: cursor-1"); + }); + + it("dry-runs package artifact metadata backfill by default", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + scanned: 20, + updated: 3, + nextCursor: "cursor-1", + done: false, + dryRun: true, + }); + + await cmdBackfillPackageArtifacts(makeOpts(), { batchSize: 20 }); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/packages/backfill/artifacts", + token: "tkn", + body: { + cursor: null, + batchSize: 20, + dryRun: true, + }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith( + "Dry run package artifact backfill: scanned 20, would update 3.", + ); + expect(mockLog).toHaveBeenCalledWith("Next cursor: cursor-1"); + }); + + it("can apply package artifact backfill across all pages", async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + ok: true, + scanned: 100, + updated: 8, + nextCursor: "cursor-2", + done: false, + dryRun: false, + }) + .mockResolvedValueOnce({ + ok: true, + scanned: 5, + updated: 1, + nextCursor: null, + done: true, + dryRun: false, + }); + + await cmdBackfillPackageArtifacts(makeOpts(), { apply: true, all: true, batchSize: 100 }); + + expect(httpMocks.apiRequest).toHaveBeenCalledTimes(2); + expect(httpMocks.apiRequest.mock.calls[0]?.[1]).toMatchObject({ + body: { cursor: null, batchSize: 100, dryRun: false }, + }); + expect(httpMocks.apiRequest.mock.calls[1]?.[1]).toMatchObject({ + body: { cursor: "cursor-2", batchSize: 100, dryRun: false }, + }); + expect(mockLog).toHaveBeenCalledWith( + "Applied package artifact backfill: scanned 105, updated 9.", + ); + }); + + it("prints package readiness checks", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + isOfficial: true, + latestVersion: "1.2.3", + }, + ready: false, + checks: [ + { + id: "clawpack", + label: "ClawPack artifact", + status: "fail", + message: "Latest version is legacy ZIP-only.", + }, + ], + blockers: ["clawpack"], + }); + + await cmdPackageReadiness(makeOpts(), "@scope/demo"); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "GET", + path: "/api/v1/packages/%40scope%2Fdemo/readiness", + token: undefined, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("@scope/demo readiness: blocked"); + expect(mockLog).toHaveBeenCalledWith("FAIL clawpack: Latest version is legacy ZIP-only."); + expect(mockLog).toHaveBeenCalledWith("Blockers: clawpack"); + }); + + it("prints package migration status checks", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + package: { + name: "@scope/demo", + displayName: "Demo", + family: "code-plugin", + isOfficial: true, + latestVersion: "1.2.3", + }, + ready: true, + checks: [ + { + id: "clawpack", + label: "ClawPack artifact", + status: "pass", + message: "Latest version has a ClawPack artifact.", + }, + ], + blockers: [], + }); + + await cmdPackageMigrationStatus(makeOpts(), "@scope/demo"); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "GET", + path: "/api/v1/packages/%40scope%2Fdemo/readiness", + token: undefined, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("@scope/demo migration: ready"); + expect(mockLog).toHaveBeenCalledWith("Version: 1.2.3"); + expect(mockLog).toHaveBeenCalledWith("Official: yes"); + expect(mockLog).toHaveBeenCalledWith("PASS clawpack: Latest version has a ClawPack artifact."); + }); + + it("lists package migration rows", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + items: [ + { + migrationId: "officialPluginMigrations:1", + bundledPluginId: "core.search", + packageName: "@scope/demo", + packageId: "pkg_1", + owner: "platform", + sourceRepo: "openclaw/openclaw", + sourcePath: "plugins/search", + sourceCommit: "abc123", + phase: "blocked", + blockers: ["missing ClawPack"], + hostTargetsComplete: true, + scanClean: false, + moderationApproved: false, + runtimeBundlesReady: false, + notes: "needs publisher upload", + createdAt: 100, + updatedAt: 200, + }, + ], + nextCursor: null, + done: true, + }); + + await cmdListPackageMigrations(makeOpts(), { phase: "blocked", limit: 10 }); + + const url = new URL(httpMocks.apiRequest.mock.calls[0]?.[1].url as string); + expect(url.pathname).toBe("/api/v1/packages/migrations"); + expect(url.searchParams.get("phase")).toBe("blocked"); + expect(url.searchParams.get("limit")).toBe("10"); + expect(mockLog).toHaveBeenCalledWith("core.search blocked @scope/demo blockers:1"); + expect(mockLog).toHaveBeenCalledWith(" source: openclaw/openclaw plugins/search abc123"); + expect(mockLog).toHaveBeenCalledWith(" notes: needs publisher upload"); + }); + + it("upserts package migration rows", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + migration: { + migrationId: "officialPluginMigrations:1", + bundledPluginId: "core.search", + packageName: "@scope/demo", + packageId: "pkg_1", + owner: "platform", + sourceRepo: "openclaw/openclaw", + sourcePath: "plugins/search", + sourceCommit: null, + phase: "blocked", + blockers: ["missing ClawPack"], + hostTargetsComplete: true, + scanClean: false, + moderationApproved: false, + runtimeBundlesReady: false, + notes: null, + createdAt: 100, + updatedAt: 200, + }, + }); + + await cmdUpsertPackageMigration(makeOpts(), "core.search", { + package: "@scope/demo", + owner: "platform", + sourceRepo: "openclaw/openclaw", + sourcePath: "plugins/search", + phase: "blocked", + blockers: "missing ClawPack", + hostTargetsComplete: true, + }); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/packages/migrations", + token: "tkn", + body: { + bundledPluginId: "core.search", + packageName: "@scope/demo", + owner: "platform", + sourceRepo: "openclaw/openclaw", + sourcePath: "plugins/search", + phase: "blocked", + blockers: ["missing ClawPack"], + hostTargetsComplete: true, + }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("OK. Migration core.search is blocked for @scope/demo."); + }); + + it("publishes a code plugin package with an exact explicit payload", async () => { + const workdir = await makeTmpWorkdir(); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(123_456_789); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + files: ["dist", "openclaw.plugin.json"], + }), + "utf8", + ); + await writeFile(join(folder, ".gitignore"), "dist/\n", "utf8"); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + owner: "@openclaw", + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + sourceRef: "refs/tags/v1.0.0", + clawscanNote: "This plugin shells out only to the bundled helper binary.", + }); + + expect(getPublishPayload()).toEqual({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + ownerHandle: "openclaw", + family: "code-plugin", + version: "1.0.0", + changelog: "", + clawScanNote: "This plugin shells out only to the bundled helper binary.", + tags: ["latest"], + source: { + kind: "github", + url: "https://github.com/openclaw/demo-plugin", + repo: "openclaw/demo-plugin", + ref: "refs/tags/v1.0.0", + commit: "abc123", + path: ".", + importedAt: 123_456_789, + }, + }); + expect(getUploadedFileNames()).toEqual([]); + expect(getUploadedClawPackNames()).toEqual(["scope-demo-plugin-1.0.0.tgz"]); + expect(httpMocks.apiRequestForm.mock.calls[0]?.[1]).toEqual( + expect.objectContaining({ retryCount: 5 }), + ); + const uploadedPack = getUploadedClawPacks()[0]; + if (!uploadedPack) throw new Error("Missing uploaded ClawPack"); + const parsed = parseClawPack(new Uint8Array(await uploadedPack.arrayBuffer())); + expect(parsed.entries.map((entry) => entry.path).sort()).toEqual([ + "dist/index.js", + "openclaw.plugin.json", + "package.json", + ]); + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + "OK. Published @scope/demo-plugin@1.0.0 (rel_1)", + ); + expect(uiMocks.spinner.fail).not.toHaveBeenCalled(); + expect(mockLog).not.toHaveBeenCalled(); + expect(mockWrite).not.toHaveBeenCalled(); + dateSpy.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("resolves package publish dot paths from the caller cwd before the OpenClaw workdir", async () => { + const workspace = await makeTmpWorkdir(); + const pluginRoot = await makeTmpWorkdir(); + const previousCwd = process.cwd(); + try { + await mkdir(join(pluginRoot, "dist"), { recursive: true }); + await writeFile( + join(pluginRoot, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/cwd-plugin", + displayName: "Cwd Plugin", + version: "1.0.0", + files: ["dist", "openclaw.plugin.json"], + }), + "utf8", + ); + await writeFile( + join(pluginRoot, "openclaw.plugin.json"), + JSON.stringify({ id: "cwd.plugin", configSchema: { type: "object" } }), + "utf8", + ); + await writeFile(join(pluginRoot, "dist", "index.js"), "export const demo = true;\n", "utf8"); + + process.chdir(pluginRoot); + + await cmdPublishPackage(makeOpts(workspace), ".", { + dryRun: true, + sourceRepo: "openclaw/cwd-plugin", + sourceCommit: "abc123", + }); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toContain("Name: @scope/cwd-plugin"); + expect(output).toContain("Files: 3"); + } finally { + process.chdir(previousCwd); + await rm(pluginRoot, { recursive: true, force: true }); + await rm(workspace, { recursive: true, force: true }); + } + }); + + it("rejects oversized clawscan notes before uploading package files", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + await expect( + cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + clawscanNote: "x".repeat(MAX_CLAWSCAN_NOTE_CHARS + 1), + }), + ).rejects.toThrow(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("publishes a ClawPack tarball without uploading extracted files", async () => { + const workdir = await makeTmpWorkdir(); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(123_456_789); + try { + const packName = "demo-plugin-1.0.0.tgz"; + await writeFile( + join(workdir, packName), + npmPackFixture({ + "package/package.json": makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), + "package/dist/index.js": "export const demo = true;\n", + }), + ); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), packName, { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + expect(getPublishPayload()).toEqual({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + version: "1.0.0", + changelog: "", + tags: ["latest"], + source: { + kind: "github", + url: "https://github.com/openclaw/demo-plugin", + repo: "openclaw/demo-plugin", + ref: "abc123", + commit: "abc123", + path: ".", + importedAt: 123_456_789, + }, + }); + expect(getUploadedClawPackNames()).toEqual([packName]); + expect(getUploadedFileNames()).toEqual([]); + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + "OK. Published @scope/demo-plugin@1.0.0 (rel_1)", + ); + dateSpy.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("packs a plugin folder through npm pack and validates the ClawPack", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await mkdir(join(workdir, "packs"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + description: "Demo plugin", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); + + await cmdPackPackage(makeOpts(workdir), "demo-plugin", { + packDestination: "packs", + }); + + const packPath = join(workdir, "packs", "scope-demo-plugin-1.0.0.tgz"); + const parsed = parseClawPack(new Uint8Array(await readFile(packPath))); + expect(parsed.packageName).toBe("@scope/demo-plugin"); + expect(parsed.packageVersion).toBe("1.0.0"); + expect(parsed.entries.map((entry) => entry.path)).toContain("openclaw.plugin.json"); + expect(mockLog).toHaveBeenCalledWith(`Path: ${packPath}`); + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + `Packed @scope/demo-plugin@1.0.0 -> ${packPath}`, + ); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects a code plugin ClawPack with TypeScript entries and no compiled runtime", async () => { + const workdir = await makeTmpWorkdir(); + try { + const packName = "demo-plugin-1.0.0.tgz"; + await writeFile( + join(workdir, packName), + npmPackFixture({ + "package/package.json": makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + openclaw: { + extensions: ["./index.ts"], + compat: { + pluginApi: ">=2026.3.24-beta.2", + }, + build: { + openclawVersion: "2026.3.24-beta.2", + }, + }, + }), + "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), + "package/index.ts": "export const demo = true;\n", + }), + ); + + await expect( + cmdPublishPackage(makeOpts(workdir), packName, { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }), + ).rejects.toThrow( + "@scope/demo-plugin requires compiled runtime output for TypeScript entry ./index.ts", + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects a ClawPack tarball without openclaw.plugin.json", async () => { + const workdir = await makeTmpWorkdir(); + try { + const packName = "demo-plugin-1.0.0.tgz"; + await writeFile( + join(workdir, packName), + npmPackFixture({ + "package/package.json": makeCodePluginPackageJson({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "package/dist/index.js": "export const demo = true;\n", + }), + ); + + await expect( + cmdPublishPackage(makeOpts(workdir), packName, { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }), + ).rejects.toThrow("ClawPack must contain package/openclaw.plugin.json"); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("mints a short-lived publish token from GitHub Actions OIDC in CI", async () => { + const workdir = await makeTmpWorkdir(); + try { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ value: "github-oidc-jwt" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + httpMocks.apiRequest.mockResolvedValueOnce({ + token: "clh_short_publish", + expiresAt: 1_234_567_890, + }); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledWith( + new URL("https://token.actions.githubusercontent.com/oidc?audience=clawhub"), + expect.objectContaining({ + method: "GET", + headers: expect.objectContaining({ + Authorization: "Bearer gh-request-token", + }), + }), + ); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "POST", + path: "/api/v1/publish/token/mint", + body: { + packageName: "@scope/demo-plugin", + version: "1.0.0", + githubOidcToken: "github-oidc-jwt", + }, + }), + expect.anything(), + ); + const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as + | { token?: string } + | undefined; + expect(publishArgs?.token).toBe("clh_short_publish"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("uses normal token auth for manual override publishes", async () => { + const workdir = await makeTmpWorkdir(); + try { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; + const fetchMock = vi.fn(); + vi.stubGlobal("fetch", fetchMock); + + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + authTokenMocks.requireAuthToken.mockResolvedValueOnce("manual-token"); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + manualOverrideReason: "break glass", + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(httpMocks.apiRequest).not.toHaveBeenCalled(); + const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as + | { token?: string; form?: FormData } + | undefined; + expect(publishArgs?.token).toBe("manual-token"); + const payloadEntry = publishArgs?.form?.get("payload"); + if (typeof payloadEntry !== "string") { + throw new Error("Missing publish payload"); + } + expect(JSON.parse(payloadEntry)).toMatchObject({ + manualOverrideReason: "break glass", + }); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("falls back to a normal auth token when trusted minting is unavailable", async () => { + const workdir = await makeTmpWorkdir(); + try { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ value: "github-oidc-jwt" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + authTokenMocks.requireAuthToken.mockResolvedValueOnce("fallback-token"); + httpMocks.apiRequest.mockRejectedValueOnce( + Object.assign(new Error("Trusted publisher config is not set"), { status: 403 }), + ); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as + | { token?: string } + | undefined; + expect(publishArgs?.token).toBe("fallback-token"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("falls back to a normal auth token when trusted minting returns a 400", async () => { + const workdir = await makeTmpWorkdir(); + try { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ value: "github-oidc-jwt" }), { + status: 200, + headers: { "content-type": "application/json" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + authTokenMocks.requireAuthToken.mockResolvedValueOnce("fallback-token"); + httpMocks.apiRequest.mockRejectedValueOnce( + Object.assign(new Error("Trusted publishing requires workflow_dispatch"), { status: 400 }), + ); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as + | { token?: string } + | undefined; + expect(publishArgs?.token).toBe("fallback-token"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("falls back to a normal auth token when requesting the GitHub OIDC token fails", async () => { + const workdir = await makeTmpWorkdir(); + try { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; + const fetchMock = vi.fn().mockResolvedValue( + new Response("oidc unavailable", { + status: 500, + statusText: "Internal Server Error", + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + authTokenMocks.requireAuthToken.mockResolvedValueOnce("fallback-token"); + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as + | { token?: string } + | undefined; + expect(publishArgs?.token).toBe("fallback-token"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("publishes a bundle plugin package with real bundle marker detection", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-bundle"); + await mkdir(join(folder, "dist"), { recursive: true }); + await mkdir(join(folder, ".codex-plugin"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "demo-bundle", + displayName: "Demo Bundle", + version: "0.4.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.bundle" }), + "utf8", + ); + await writeFile( + join(folder, ".codex-plugin", "plugin.json"), + JSON.stringify({ name: "Demo Bundle", skills: ["skills"] }), + "utf8", + ); + await writeFile(join(folder, "dist", "plugin.wasm"), "binary", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_bundle", + releaseId: "rel_bundle", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-bundle", { + bundleFormat: "openclaw-bundle", + hostTargets: "desktop,mobile", + }); + + expect(getPublishPayload()).toEqual({ + name: "demo-bundle", + displayName: "Demo Bundle", + family: "bundle-plugin", + version: "0.4.0", + changelog: "", + tags: ["latest"], + bundle: { + format: "openclaw-bundle", + hostTargets: ["desktop", "mobile"], + }, + }); + expect(getUploadedFileNames()).toEqual([ + ".codex-plugin/plugin.json", + "dist/plugin.wasm", + "openclaw.plugin.json", + "package.json", + ]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects code-plugin publish without source metadata", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + await expect(cmdPublishPackage(makeOpts(workdir), "demo-plugin", {})).rejects.toThrow( + "--source-repo and --source-commit required for code plugins", + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects code-plugin publish when openclaw.plugin.json is missing", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ name: "demo-plugin", displayName: "Demo", version: "1.0.0" }), + "utf8", + ); + + await expect( + cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + family: "code-plugin", + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }), + ).rejects.toThrow("openclaw.plugin.json required"); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects code-plugin publish when required OpenClaw compatibility metadata is missing", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + openclaw: { + extensions: ["./index.ts"], + }, + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin", configSchema: { type: "object" } }), + "utf8", + ); + + await expect( + cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }), + ).rejects.toThrow( + "openclaw.compat.pluginApi is required for external code plugins published to ClawHub.", + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("publishes code plugins when host targets are missing", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + openclaw: { + extensions: ["./index.ts"], + compat: { pluginApi: ">=2026.3.24-beta.2" }, + build: { openclawVersion: "2026.3.24-beta.2" }, + environment: {}, + }, + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin", configSchema: { type: "object" } }), + "utf8", + ); + await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: "abc123", + }); + + expect(getPublishPayload()).toMatchObject({ + name: "demo-plugin", + family: "code-plugin", + version: "1.0.0", + }); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects bundle-plugin publish when openclaw.plugin.json is missing", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-bundle"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ name: "demo-bundle", displayName: "Demo Bundle", version: "0.1.0" }), + "utf8", + ); + + await expect( + cmdPublishPackage(makeOpts(workdir), "demo-bundle", { family: "bundle-plugin" }), + ).rejects.toThrow("openclaw.plugin.json required"); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("respects package ignore rules and built-in ignored directories", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "ignored-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await mkdir(join(folder, "node_modules", "pkg"), { recursive: true }); + await mkdir(join(folder, ".git"), { recursive: true }); + await mkdir(join(folder, ".codex-plugin"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ + name: "ignored-plugin", + displayName: "Ignored Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "ignored.plugin" }), + "utf8", + ); + await writeFile( + join(folder, ".codex-plugin", "plugin.json"), + JSON.stringify({ name: "Ignored Plugin", skills: ["skills"] }), + "utf8", + ); + await writeFile(join(folder, ".clawhubignore"), "ignored.txt\n", "utf8"); + await writeFile(join(folder, "dist", "index.js"), "export {};\n", "utf8"); + await writeFile(join(folder, "ignored.txt"), "ignore me\n", "utf8"); + await writeFile( + join(folder, "node_modules", "pkg", "index.js"), + "module.exports = {};\n", + "utf8", + ); + await writeFile(join(folder, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_ignored", + releaseId: "rel_ignored", + }); + + await cmdPublishPackage(makeOpts(workdir), "ignored-plugin", { + sourceRepo: "openclaw/ignored-plugin", + sourceCommit: "abc123", + }); + + expect(getUploadedFileNames()).toEqual([ + ".clawhubignore", + ".codex-plugin/plugin.json", + "dist/index.js", + "openclaw.plugin.json", + "package.json", + ]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("reports publish failures through the spinner without writing to stdout", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "broken-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "broken-plugin", + displayName: "Broken Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "broken.plugin" }), + "utf8", + ); + + httpMocks.apiRequestForm.mockRejectedValueOnce(new Error("Registry rejected upload")); + + await expect( + cmdPublishPackage(makeOpts(workdir), "broken-plugin", { + sourceRepo: "openclaw/broken-plugin", + sourceCommit: "deadbeef", + }), + ).rejects.toThrow("Registry rejected upload"); + + expect(uiMocks.spinner.fail).toHaveBeenCalledWith("Registry rejected upload"); + expect(uiMocks.spinner.succeed).not.toHaveBeenCalled(); + expect(mockLog).not.toHaveBeenCalled(); + expect(mockWrite).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("auto-detects local git source metadata and matches the explicit payload", async () => { + const workdir = await makeTmpWorkdir(); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(987_654_321); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); + + runGit(folder, ["init", "-b", "main"]); + runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); + runGit(folder, ["add", "."]); + runGit(folder, [ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-m", + "init", + ]); + const commit = runGit(folder, ["rev-parse", "HEAD"]); + runGit(folder, ["-c", "tag.gpgSign=false", "tag", "v1.0.0"]); + + httpMocks.apiRequestForm.mockResolvedValue({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/demo-plugin", + sourceCommit: commit, + sourceRef: "v1.0.0", + }); + const explicitPayload = getPublishPayload(); + const explicitFiles = getUploadedFileNames(); + + httpMocks.apiRequestForm.mockClear(); + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", {}); + const inferredPayload = getPublishPayload(); + const inferredFiles = getUploadedFileNames(); + + expect(inferredPayload).toEqual(explicitPayload); + expect(inferredFiles).toEqual(explicitFiles); + dateSpy.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("lets explicit source flags override inferred git metadata", async () => { + const workdir = await makeTmpWorkdir(); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(222_222_222); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + runGit(folder, ["init", "-b", "main"]); + runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); + runGit(folder, ["add", "."]); + runGit(folder, [ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-m", + "init", + ]); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { + sourceRepo: "openclaw/override-plugin", + sourceCommit: "feedface", + sourceRef: "refs/heads/release", + sourcePath: "custom/path", + }); + + expect(getPublishPayload()).toEqual({ + name: "demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + version: "1.0.0", + changelog: "", + tags: ["latest"], + source: { + kind: "github", + url: "https://github.com/openclaw/override-plugin", + repo: "openclaw/override-plugin", + ref: "refs/heads/release", + commit: "feedface", + path: "custom/path", + importedAt: 222_222_222, + }, + }); + dateSpy.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("preserves inferred source subpaths for nested local plugin folders", async () => { + const workdir = await makeTmpWorkdir(); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(333_333_333); + try { + const folder = join(workdir, "packages", "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin", configSchema: { type: "object" } }), + "utf8", + ); + + runGit(workdir, ["init", "-b", "main"]); + runGit(workdir, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); + runGit(workdir, ["add", "."]); + runGit(workdir, [ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-m", + "init", + ]); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "packages/demo-plugin", {}); + + expect(getPublishPayload()).toEqual({ + name: "demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + version: "1.0.0", + changelog: "", + tags: ["latest"], + source: { + kind: "github", + url: "https://github.com/openclaw/demo-plugin", + repo: "openclaw/demo-plugin", + ref: "main", + commit: expect.any(String), + path: "packages/demo-plugin", + importedAt: 333_333_333, + }, + }); + dateSpy.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("uses --source-path as the package folder for GitHub shorthand sources", async () => { + const workdir = await makeTmpWorkdir(); + const originalFetch = globalThis.fetch; + const commit = "0123456789abcdef0123456789abcdef01234567"; + const archiveBytes = zipSync({ + "repo-root/plugins/demo/package.json": new TextEncoder().encode( + makeCodePluginPackageJson({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + ), + "repo-root/plugins/demo/openclaw.plugin.json": new TextEncoder().encode( + JSON.stringify({ id: "demo.plugin" }), + ), + "repo-root/plugins/demo/dist/index.js": new TextEncoder().encode("export {};\n"), + "repo-root/other/package.json": new TextEncoder().encode('{"name":"wrong"}\n'), + }); + const archiveBody = archiveBytes.buffer.slice( + archiveBytes.byteOffset, + archiveBytes.byteOffset + archiveBytes.byteLength, + ) as ArrayBuffer; + const fetchMock = vi.fn(async (input) => { + const url = input instanceof Request ? input.url : input.toString(); + if (url.endsWith("/repos/owner/repo/commits/main")) { + return new Response(JSON.stringify({ sha: commit }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (url.endsWith(`/repos/owner/repo/zipball/${commit}`)) { + return new Response(archiveBody, { + status: 200, + headers: { "content-type": "application/zip" }, + }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + + Object.defineProperty(globalThis, "fetch", { + value: fetchMock, + configurable: true, + writable: true, + }); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(555_555_555); + + try { + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + packageId: "pkg_1", + releaseId: "rel_1", + }); + + await cmdPublishPackage(makeOpts(workdir), "owner/repo@main", { + sourcePath: "plugins/demo", + }); + + expect(getUploadedFileNames()).toEqual([]); + expect(getUploadedClawPackNames()).toEqual(["scope-demo-plugin-1.0.0.tgz"]); + expect(getPublishPayload()).toEqual({ + name: "@scope/demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + version: "1.0.0", + changelog: "", + tags: ["latest"], + source: { + kind: "github", + url: "https://github.com/owner/repo", + repo: "owner/repo", + ref: "main", + commit, + path: "plugins/demo", + importedAt: 555_555_555, + }, + }); + } finally { + Object.defineProperty(globalThis, "fetch", { + value: originalFetch, + configurable: true, + writable: true, + }); + dateSpy.mockRestore(); + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("supports dry-run without auth or publish and prints a summary", async () => { + const workdir = await makeTmpWorkdir(); + const dateSpy = vi.spyOn(Date, "now").mockReturnValue(444_444_444); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(join(folder, "dist"), { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); + + runGit(folder, ["init", "-b", "main"]); + runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); + runGit(folder, ["add", "."]); + runGit(folder, [ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-m", + "init", + ]); + const commit = runGit(folder, ["rev-parse", "HEAD"]); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { dryRun: true }); + + expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + expect(mockLog.mock.calls.map((call) => call[0])).toEqual( + expect.arrayContaining([ + "Dry run - nothing will be published.", + expect.stringMatching(/Source:\s+github:openclaw\/demo-plugin@main/), + expect.stringMatching(/Name:\s+demo-plugin/), + expect.stringMatching(new RegExp(`Commit:\\s+${commit}`)), + "Files:", + ]), + ); + expect(mockWrite).not.toHaveBeenCalled(); + dateSpy.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("supports dry-run json output without auth or publish", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + makeCodePluginPackageJson({ + name: "demo-plugin", + displayName: "Demo Plugin", + version: "1.0.0", + }), + "utf8", + ); + await writeFile( + join(folder, "openclaw.plugin.json"), + JSON.stringify({ id: "demo.plugin" }), + "utf8", + ); + + runGit(folder, ["init", "-b", "main"]); + runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); + runGit(folder, ["add", "."]); + runGit(folder, [ + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "-m", + "init", + ]); + + await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { dryRun: true, json: true }); + + expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + expect(mockLog).not.toHaveBeenCalled(); + expect(mockWrite).toHaveBeenCalledTimes(1); + const output = String(mockWrite.mock.calls[0]?.[0] ?? "").trim(); + expect(JSON.parse(output)).toEqual({ + source: "github:openclaw/demo-plugin@main", + name: "demo-plugin", + displayName: "Demo Plugin", + family: "code-plugin", + version: "1.0.0", + commit: expect.any(String), + files: 2, + totalBytes: expect.any(Number), + }); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("gets trusted publisher config for a package", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + trustedPublisher: { + provider: "github-actions", + repository: "openclaw/openclaw", + repositoryId: "1", + repositoryOwner: "openclaw", + repositoryOwnerId: "2", + workflowFilename: "plugin-clawhub-release.yml", + environment: "clawhub-release", + }, + }); + + await cmdGetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo"); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "GET", + path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", + }), + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("Provider: github-actions"); + expect(mockLog).toHaveBeenCalledWith("Repository: openclaw/openclaw"); + expect(mockLog).toHaveBeenCalledWith("Workflow: plugin-clawhub-release.yml"); + expect(mockLog).toHaveBeenCalledWith("Environment: clawhub-release"); + }); + + it("gets trusted publisher config without a pinned environment", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + trustedPublisher: { + provider: "github-actions", + repository: "openclaw/openclaw", + repositoryId: "1", + repositoryOwner: "openclaw", + repositoryOwnerId: "2", + workflowFilename: "plugin-clawhub-release.yml", + }, + }); + + await cmdGetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo"); + + expect(mockLog).toHaveBeenCalledWith("Provider: github-actions"); + expect(mockLog).toHaveBeenCalledWith("Repository: openclaw/openclaw"); + expect(mockLog).toHaveBeenCalledWith("Workflow: plugin-clawhub-release.yml"); + expect(mockLog).not.toHaveBeenCalledWith(expect.stringContaining("Environment:")); + }); + + it("sets trusted publisher config for a package", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + trustedPublisher: { + provider: "github-actions", + repository: "openclaw/openclaw", + repositoryId: "1", + repositoryOwner: "openclaw", + repositoryOwnerId: "2", + workflowFilename: "plugin-clawhub-release.yml", + environment: "clawhub-release", + }, + }); + + await cmdSetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo", { + repository: "openclaw/openclaw", + workflowFilename: "plugin-clawhub-release.yml", + environment: "clawhub-release", + }); + + expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "POST", + path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", + token: "tkn", + body: { + repository: "openclaw/openclaw", + workflowFilename: "plugin-clawhub-release.yml", + environment: "clawhub-release", + }, + }), + expect.anything(), + ); + }); + + it("sets trusted publisher config for a package without environment", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + trustedPublisher: { + provider: "github-actions", + repository: "openclaw/openclaw", + repositoryId: "1", + repositoryOwner: "openclaw", + repositoryOwnerId: "2", + workflowFilename: "plugin-clawhub-release.yml", + }, + }); + + await cmdSetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo", { + repository: "openclaw/openclaw", + workflowFilename: "plugin-clawhub-release.yml", + }); + + expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "POST", + path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", + token: "tkn", + body: { + repository: "openclaw/openclaw", + workflowFilename: "plugin-clawhub-release.yml", + }, + }), + expect.anything(), + ); + }); + + it("deletes trusted publisher config for a package", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + + await cmdDeletePackageTrustedPublisher(makeOpts(), "@openclaw/zalo"); + + expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "DELETE", + path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", + token: "tkn", + }), + undefined, + ); + }); + + it("soft-deletes a package with confirmation bypass", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + + await cmdDeletePackage(makeOpts(), "@openclaw/zalo", { yes: true }, false); + + expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "DELETE", + path: "/api/v1/packages/%40openclaw%2Fzalo", + token: "tkn", + }), + expect.anything(), + ); + }); + + it("transfers a package to another publisher", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + packageId: "packages:opik", + name: "@opik/opik-openclaw", + ownerUserId: "users:vincent", + ownerPublisherId: "publishers:opik", + channel: "community", + isOfficial: false, + }); + + await cmdTransferPackage(makeOpts(), "@opik/opik-openclaw", { to: "opik" }); + + expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "POST", + path: "/api/v1/packages/%40opik%2Fopik-openclaw/transfer", + token: "tkn", + body: { toOwner: "opik" }, + }), + expect.anything(), + ); + }); + + it("requires --yes for non-interactive package deletes", async () => { + await expect(cmdDeletePackage(makeOpts(), "@openclaw/zalo", {}, false)).rejects.toThrow( + /--yes/i, + ); + expect(httpMocks.apiRequest).not.toHaveBeenCalled(); + }); + + it("restores package deletes through the undelete endpoint", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + + await cmdUndeletePackage(makeOpts(), "@openclaw/zalo", { yes: true }, false); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "POST", + path: "/api/v1/packages/%40openclaw%2Fzalo/undelete", + token: "tkn", + }), + expect.anything(), + ); + }); +}); diff --git a/dt-skill/src/cli/commands/packages.ts b/dt-skill/src/cli/commands/packages.ts new file mode 100644 index 00000000..53a9a944 --- /dev/null +++ b/dt-skill/src/cli/commands/packages.ts @@ -0,0 +1,2075 @@ +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, dirname, join, relative, resolve, sep } from "node:path"; +import ignore from "ignore"; +import mime from "mime"; +import semver from "semver"; +import { parseClawPack } from "../../clawpack.js"; +import { apiRequest, apiRequestForm, fetchBinary, fetchText, registryUrl } from "../../http.js"; +import { + ApiRoutes, + ApiV1DeleteResponseSchema, + ApiV1PackageArtifactResponseSchema, + ApiV1PackageListResponseSchema, + ApiV1PackageModerationStatusResponseSchema, + ApiV1PackagePublishResponseSchema, + ApiV1PackageReadinessResponseSchema, + ApiV1PackageReportResponseSchema, + ApiV1PackageResponseSchema, + ApiV1PackageSearchResponseSchema, + ApiV1PackageTransferResponseSchema, + ApiV1PackageTrustedPublisherResponseSchema, + ApiV1PackageVersionListResponseSchema, + ApiV1PackageVersionResponseSchema, + ApiV1PublishTokenMintResponseSchema, + normalizeClawScanNote, + normalizeOpenClawExternalPluginCompatibility, + type PackageArtifactSummary, + type PackageCapabilitySummary, + type PackageCompatibility, + type PackageFamily, + type PackageTrustedPublisher, + type PackageVerificationSummary, + validateOpenClawExternalCodePluginPackageContents, + validateOpenClawExternalCodePluginPackageJson, +} from "../../schema/index.js"; +import { getOptionalAuthToken, requireAuthToken } from "../authToken.js"; +import { getRegistry } from "../registry.js"; +import { titleCase } from "../slug.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; +import { + fetchGitHubSource, + normalizeGitHubRepo, + resolveLocalGitInfo, + resolveSourceInput, +} from "./github.js"; + +const DOT_DIR = ".clawhub"; +const LEGACY_DOT_DIR = ".clawdhub"; +const DOT_IGNORE = ".clawhubignore"; +const LEGACY_DOT_IGNORE = ".clawdhubignore"; +const MAX_CLAWPACK_BYTES = 120 * 1024 * 1024; +const PACKAGE_PUBLISH_RETRY_COUNT = 5; + +type PackageInspectOptions = { + version?: string; + tag?: string; + versions?: boolean; + limit?: number; + files?: boolean; + file?: string; + json?: boolean; +}; + +type PackageExploreOptions = { + family?: PackageFamily; + official?: boolean; + executesCode?: boolean; + target?: string; + os?: string; + arch?: string; + libc?: string; + requiresBrowser?: boolean; + requiresDesktop?: boolean; + requiresNativeDeps?: boolean; + requiresExternalService?: boolean; + externalService?: string; + binary?: string; + osPermission?: string; + artifactKind?: "legacy-zip" | "npm-pack"; + npmMirror?: boolean; + limit?: number; + json?: boolean; +}; + +type PackagePublishOptions = { + family?: "code-plugin" | "bundle-plugin"; + name?: string; + displayName?: string; + owner?: string; + version?: string; + changelog?: string; + clawscanNote?: string; + manualOverrideReason?: string; + tags?: string; + bundleFormat?: string; + hostTargets?: string; + sourceRepo?: string; + sourceCommit?: string; + sourceRef?: string; + sourcePath?: string; + dryRun?: boolean; + json?: boolean; +}; + +type PackagePackOptions = { + packDestination?: string; + json?: boolean; +}; + +type PackageDownloadOptions = { + version?: string; + tag?: string; + output?: string; + force?: boolean; + json?: boolean; +}; + +type PackageVerifyOptions = { + packageName?: string; + version?: string; + tag?: string; + sha256?: string; + npmIntegrity?: string; + npmShasum?: string; + json?: boolean; +}; + +type PackageReportOptions = { + version?: string; + reason?: string; + json?: boolean; +}; + +type PackageModerationStatusOptions = { + json?: boolean; +}; + +type PackageReadinessOptions = { + json?: boolean; +}; + +type PackageMigrationStatusOptions = PackageReadinessOptions; + +type PackageTrustedPublisherGetOptions = { + json?: boolean; +}; + +type PackageDeleteOptions = { + yes?: boolean; + json?: boolean; +}; + +type PackageUndeleteOptions = PackageDeleteOptions; + +type PackageTransferOptions = { + to: string; + reason?: string; + json?: boolean; +}; + +type PackageFile = { + relPath: string; + bytes: Uint8Array; + contentType?: string; +}; + +type InferredPublishSource = { + repo?: string; + commit?: string; + ref?: string; + path?: string; + url?: string; +}; + +type PackagePublishSource = ReturnType; + +type PackagePublishPayload = { + name: string; + displayName: string; + ownerHandle?: string; + family: "code-plugin" | "bundle-plugin"; + version: string; + changelog: string; + clawScanNote?: string; + manualOverrideReason?: string; + tags: string[]; + source?: NonNullable; + bundle?: { + format?: string; + hostTargets: string[]; + }; +}; + +type PackagePublishPlan = { + folder: string; + cleanup?: () => Promise; + filesOnDisk: PackageFile[]; + clawpackOnDisk?: PackageFile; + packageJson?: unknown; + payload: PackagePublishPayload; + compatibility?: PackageCompatibility; + sourceLabel: string; + output: { + source: string; + name: string; + displayName: string; + family: "code-plugin" | "bundle-plugin"; + version: string; + commit?: string; + files: number; + totalBytes: number; + }; +}; + +type PackedClawPack = { + path: string; + file: PackageFile; + parsed: ReturnType; + identity: ArtifactIdentity; +}; + +function appendPackageExploreFilters(url: URL, options: PackageExploreOptions) { + if (options.target) url.searchParams.set("target", options.target); + if (options.os) url.searchParams.set("os", options.os); + if (options.arch) url.searchParams.set("arch", options.arch); + if (options.libc) url.searchParams.set("libc", options.libc); + if (options.requiresBrowser) url.searchParams.set("requiresBrowser", "true"); + if (options.requiresDesktop) url.searchParams.set("requiresDesktop", "true"); + if (options.requiresNativeDeps) url.searchParams.set("requiresNativeDeps", "true"); + if (options.requiresExternalService) url.searchParams.set("requiresExternalService", "true"); + if (options.externalService) url.searchParams.set("externalService", options.externalService); + if (options.binary) url.searchParams.set("binary", options.binary); + if (options.osPermission) url.searchParams.set("osPermission", options.osPermission); + if (options.artifactKind) url.searchParams.set("artifactKind", options.artifactKind); + if (options.npmMirror) url.searchParams.set("npmMirror", "true"); +} + +type PrintableFile = { + path: string; + size: number | null; + sha256: string | null; + contentType: string | null; +}; + +type PackageResponse = Awaited>; +type PackageVersionResponse = Awaited>; +type PackageArtifactResponse = Awaited>; +type ArtifactIdentity = { + sha256: string; + npmIntegrity: string; + npmShasum: string; + byteLength: number; +}; + +export async function cmdExplorePackages( + opts: GlobalOpts, + query: string, + options: PackageExploreOptions = {}, +) { + const trimmedQuery = query.trim(); + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(trimmedQuery ? "Searching packages" : "Listing packages"); + try { + const limit = clampLimit(options.limit ?? 25, 100); + if (trimmedQuery) { + const url = registryUrl(`${ApiRoutes.packages}/search`, registry); + url.searchParams.set("q", trimmedQuery); + url.searchParams.set("limit", String(limit)); + if (options.family) url.searchParams.set("family", options.family); + if (options.official) url.searchParams.set("isOfficial", "true"); + if (typeof options.executesCode === "boolean") { + url.searchParams.set("executesCode", String(options.executesCode)); + } + appendPackageExploreFilters(url, options); + const result = await apiRequest( + registry, + { method: "GET", url: url.toString(), token }, + ApiV1PackageSearchResponseSchema, + ); + spinner.stop(); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + return; + } + if (result.results.length === 0) { + console.log("No packages found."); + return; + } + for (const entry of result.results) { + console.log(formatPackageLine(entry.package)); + } + return; + } + + const route = + options.family === "code-plugin" + ? ApiRoutes.codePlugins + : options.family === "bundle-plugin" + ? ApiRoutes.bundlePlugins + : ApiRoutes.packages; + const url = registryUrl(route, registry); + url.searchParams.set("limit", String(limit)); + if (options.family === "skill") url.searchParams.set("family", "skill"); + if (options.official) url.searchParams.set("isOfficial", "true"); + if (typeof options.executesCode === "boolean") { + url.searchParams.set("executesCode", String(options.executesCode)); + } + appendPackageExploreFilters(url, options); + const result = await apiRequest( + registry, + { method: "GET", url: url.toString(), token }, + ApiV1PackageListResponseSchema, + ); + spinner.stop(); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + return; + } + if (result.items.length === 0) { + console.log("No packages found."); + return; + } + for (const item of result.items) { + console.log(formatPackageLine(item)); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdInspectPackage( + opts: GlobalOpts, + packageName: string, + options: PackageInspectOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + if (options.version && options.tag) fail("Use either --version or --tag"); + + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Fetching package"); + try { + const detail = await apiRequestPackageDetail(registry, trimmed, token); + if (!detail.package) { + spinner.fail("Package not found"); + return; + } + + const tags = normalizeTags(detail.package.tags); + const latestVersion = detail.package.latestVersion ?? tags.latest ?? null; + const taggedVersion = options.tag ? (tags[options.tag] ?? null) : null; + if (options.tag && !taggedVersion) { + spinner.fail(`Unknown tag "${options.tag}"`); + return; + } + const requestedVersion = options.version ?? taggedVersion ?? null; + + let versionResult: PackageVersionResponse | null = null; + if (options.files || options.file || options.version || options.tag) { + const targetVersion = requestedVersion ?? latestVersion; + if (!targetVersion) fail("Could not resolve latest version"); + spinner.text = `Fetching ${trimmed}@${targetVersion}`; + versionResult = await apiRequestPackageVersion(registry, trimmed, targetVersion, token); + } + + let versionsList: Awaited> | null = null; + if (options.versions) { + const limit = clampLimit(options.limit ?? 25, 100); + spinner.text = `Fetching versions (${limit})`; + versionsList = await apiRequestPackageVersions(registry, trimmed, limit, token); + } + + let fileContent: string | null = null; + if (options.file) { + const url = registryUrl( + `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/file`, + registry, + ); + url.searchParams.set("path", options.file); + if (options.version) { + url.searchParams.set("version", options.version); + } else if (options.tag) { + url.searchParams.set("tag", options.tag); + } else if (latestVersion) { + url.searchParams.set("version", latestVersion); + } + spinner.text = `Fetching ${options.file}`; + fileContent = await fetchText(registry, { url: url.toString(), token }); + } + + spinner.stop(); + + const output = { + package: detail.package, + owner: detail.owner, + version: versionResult?.version ?? null, + versions: versionsList?.items ?? null, + file: options.file ? { path: options.file, content: fileContent } : null, + }; + + if (options.json) { + console.log(JSON.stringify(output, null, 2)); + return; + } + + const shouldPrintMeta = !options.file || options.files || options.versions || options.version; + if (shouldPrintMeta) { + printPackageSummary(detail); + } + + if (shouldPrintMeta && versionResult?.version) { + printVersionSummary(versionResult.version); + printCompatibility( + versionResult.version.compatibility ?? detail.package.compatibility ?? null, + ); + printCapabilities(versionResult.version.capabilities ?? detail.package.capabilities ?? null); + printVerification(versionResult.version.verification ?? detail.package.verification ?? null); + printArtifact(versionResult.version.artifact ?? detail.package.artifact ?? null); + } else if (shouldPrintMeta) { + printCompatibility(detail.package.compatibility ?? null); + printCapabilities(detail.package.capabilities ?? null); + printVerification(detail.package.verification ?? null); + printArtifact(detail.package.artifact ?? null); + } + + if (versionsList?.items) { + if (versionsList.items.length === 0) { + console.log("No versions found."); + } else { + console.log("Versions:"); + for (const item of versionsList.items) { + console.log(`- ${item.version} ${formatTimestamp(item.createdAt)}`); + } + } + } + + if (versionResult?.version && options.files) { + const files = normalizeFiles(versionResult.version.files); + if (files.length === 0) { + console.log("No files found."); + } else { + console.log("Files:"); + for (const file of files) { + console.log(formatFileLine(file)); + } + } + } + + if (options.file && fileContent !== null) { + if (shouldPrintMeta) console.log(`\n${options.file}:\n`); + process.stdout.write(fileContent); + if (!fileContent.endsWith("\n")) process.stdout.write("\n"); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdGetPackageTrustedPublisher( + opts: GlobalOpts, + packageName: string, + options: PackageTrustedPublisherGetOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Fetching trusted publisher"); + try { + const result = await apiRequestPackageTrustedPublisher(registry, trimmed, token); + spinner.stop(); + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + if (!result.trustedPublisher) { + console.log("No trusted publisher configured."); + return; + } + printTrustedPublisher(result.trustedPublisher); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdPackPackage( + opts: GlobalOpts, + sourceArg: string, + options: PackagePackOptions = {}, +) { + if (!sourceArg?.trim()) fail("Path required"); + const resolvedSource = await resolveSourceInput(sourceArg, { + workdir: opts.workdir, + localWorkdirs: [process.cwd(), opts.workdir], + }); + if (resolvedSource.kind !== "local") fail("Path must be a package folder"); + const sourcePath = resolvedSource.path; + const sourceStat = await stat(sourcePath).catch(() => null); + if (!sourceStat?.isDirectory()) fail("Path must be a package folder"); + + const packageJson = await readJsonFile(join(sourcePath, "package.json")); + if (!packageJson) fail("package.json required"); + const pluginManifest = await readJsonFile(join(sourcePath, "openclaw.plugin.json")); + if (!pluginManifest) fail("openclaw.plugin.json required"); + + const packageName = packageJsonString(packageJson, "name"); + const packageVersion = packageJsonString(packageJson, "version"); + if (!packageName) fail("package.json name required"); + if (!packageVersion) fail("package.json version required"); + if (!semver.valid(packageVersion)) fail("package.json version must be valid semver"); + + const validation = validateOpenClawExternalCodePluginPackageJson(packageJson); + if (validation.issues.length > 0) { + fail(validation.issues.map((issue) => issue.message).join(" ")); + } + + const packDestination = resolve(opts.workdir, options.packDestination ?? "."); + await mkdir(packDestination, { recursive: true }); + + const spinner = options.json ? null : createSpinner(`Packing ${packageName}@${packageVersion}`); + try { + const packed = await createClawPackFromFolder({ + sourcePath, + packDestination, + cwd: opts.workdir, + }); + const contentValidation = validateOpenClawExternalCodePluginPackageContents( + packed.parsed.packageJson, + packed.parsed.entries.map((entry) => entry.path), + ); + if (contentValidation.issues.length > 0) { + fail(contentValidation.issues.map((issue) => issue.message).join(" ")); + } + const output = { + path: packed.path, + name: packed.parsed.packageName, + version: packed.parsed.packageVersion, + size: packed.file.bytes.byteLength, + files: packed.parsed.entries.length, + sha256: packed.identity.sha256, + npmIntegrity: packed.identity.npmIntegrity, + npmShasum: packed.identity.npmShasum, + }; + + spinner?.succeed( + `Packed ${packed.parsed.packageName}@${packed.parsed.packageVersion} -> ${packed.path}`, + ); + if (options.json) { + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); + } else { + console.log(`Path: ${packed.path}`); + console.log(`Size: ${packed.file.bytes.byteLength} bytes`); + console.log(`SHA-256: ${packed.identity.sha256}`); + console.log(`npm integrity: ${packed.identity.npmIntegrity}`); + } + } catch (error) { + spinner?.fail(formatError(error)); + throw error; + } +} + +async function createClawPackFromFolder(options: { + sourcePath: string; + packDestination: string; + cwd: string; +}): Promise { + const result = spawnSync( + "npm", + [ + "pack", + options.sourcePath, + "--json", + "--ignore-scripts", + "--pack-destination", + options.packDestination, + ], + { + cwd: options.cwd, + encoding: "utf8", + }, + ); + if (result.error) throw result.error; + if (result.status !== 0) { + fail((result.stderr || result.stdout || "npm pack failed").trim()); + } + + let npmOutput: Array<{ filename?: string }> = []; + try { + npmOutput = JSON.parse(result.stdout) as Array<{ filename?: string }>; + } catch { + fail("npm pack did not return JSON output"); + } + const filename = npmOutput[0]?.filename; + if (!filename) fail("npm pack did not return a tarball filename"); + + const packPath = resolve(options.packDestination, filename); + const bytes = new Uint8Array(await readFile(packPath)); + assertClawPackSize(bytes.byteLength, basename(packPath)); + const parsed = parseClawPack(bytes); + return { + path: packPath, + file: { + relPath: basename(packPath), + bytes, + contentType: "application/octet-stream", + }, + parsed, + identity: computeArtifactIdentity(bytes), + }; +} + +export async function cmdPublishPackage( + opts: GlobalOpts, + sourceArg: string, + options: PackagePublishOptions = {}, +) { + if (!sourceArg?.trim()) fail("Path required"); + + let plan: PackagePublishPlan | undefined; + try { + plan = await preparePackagePublishPlan(opts, sourceArg, options); + + if (options.dryRun) { + if (options.json) { + process.stdout.write(`${JSON.stringify(plan.output, null, 2)}\n`); + } else { + printPackageDryRun({ + source: plan.sourceLabel, + family: plan.payload.family, + name: plan.payload.name, + displayName: plan.payload.displayName, + version: plan.payload.version, + commit: plan.payload.source?.commit, + compatibility: plan.compatibility, + tags: plan.payload.tags, + files: plan.filesOnDisk, + }); + } + return; + } + + if (plan.payload.family === "code-plugin") { + const validation = validateOpenClawExternalCodePluginPackageContents( + plan.packageJson, + plan.filesOnDisk.map((file) => file.relPath), + ); + if (validation.issues.length > 0) { + fail(validation.issues.map((issue) => issue.message).join(" ")); + } + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = options.json + ? null + : createSpinner(`Preparing ${plan.payload.name}@${plan.payload.version}`); + try { + const publishToken = await resolvePackagePublishToken({ + registry, + packageName: plan.payload.name, + version: plan.payload.version, + manualOverrideReason: plan.payload.manualOverrideReason, + spinner, + }); + const form = new FormData(); + form.set("payload", JSON.stringify(plan.payload)); + + if (plan.clawpackOnDisk) { + if (spinner) spinner.text = `Uploading ${plan.clawpackOnDisk.relPath}`; + const blob = new Blob([Buffer.from(plan.clawpackOnDisk.bytes)], { + type: "application/octet-stream", + }); + form.append("clawpack", blob, plan.clawpackOnDisk.relPath); + } else { + let index = 0; + for (const file of plan.filesOnDisk) { + index += 1; + if (spinner) { + spinner.text = `Uploading ${file.relPath} (${index}/${plan.filesOnDisk.length})`; + } + const blob = new Blob([Buffer.from(file.bytes)], { + type: file.contentType ?? "application/octet-stream", + }); + form.append("files", blob, file.relPath); + } + } + + if (spinner) spinner.text = `Publishing ${plan.payload.name}@${plan.payload.version}`; + const result = await apiRequestForm( + registry, + { + method: "POST", + path: ApiRoutes.packages, + token: publishToken, + form, + retryCount: PACKAGE_PUBLISH_RETRY_COUNT, + }, + ApiV1PackagePublishResponseSchema, + ); + + if (options.json) { + process.stdout.write( + `${JSON.stringify({ ...plan.output, releaseId: result.releaseId }, null, 2)}\n`, + ); + } else { + spinner?.succeed( + `OK. Published ${plan.payload.name}@${plan.payload.version} (${result.releaseId})`, + ); + } + } catch (error) { + spinner?.fail(formatError(error)); + throw error; + } + } finally { + await plan?.cleanup?.(); + } +} + +export async function cmdDownloadPackage( + opts: GlobalOpts, + packageName: string, + options: PackageDownloadOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + if (options.version && options.tag) fail("Use either --version or --tag"); + + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = options.json ? null : createSpinner("Resolving package artifact"); + try { + const targetVersion = await resolvePackageVersion(registry, trimmed, { + token, + version: options.version, + tag: options.tag, + }); + spinnerText(spinner, `Resolving ${trimmed}@${targetVersion}`); + const artifactResult = await apiRequestPackageArtifact(registry, trimmed, targetVersion, token); + spinnerText(spinner, `Downloading ${trimmed}@${targetVersion}`); + const bytes = await fetchBinary(registry, { + url: artifactResult.artifact.downloadUrl, + token, + }); + const identity = computeArtifactIdentity(bytes); + validateDownloadedArtifact(trimmed, artifactResult, bytes, identity); + + const filename = defaultArtifactFilename(trimmed, targetVersion, artifactResult.artifact); + const outputPath = await resolveArtifactOutputPath(opts, options.output, filename); + await assertOutputWritable(outputPath, Boolean(options.force)); + await writeFile(outputPath, bytes); + spinner?.stop(); + + const output = { + package: artifactResult.package.name, + version: targetVersion, + artifact: artifactResult.artifact, + path: outputPath, + bytes: bytes.byteLength, + sha256: identity.sha256, + npmIntegrity: artifactResult.artifact.kind === "npm-pack" ? identity.npmIntegrity : undefined, + npmShasum: artifactResult.artifact.kind === "npm-pack" ? identity.npmShasum : undefined, + }; + if (options.json) { + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); + return; + } + console.log(`Downloaded ${artifactResult.package.name}@${targetVersion} -> ${outputPath}`); + console.log(`Artifact: ${artifactResult.artifact.kind}`); + console.log(`SHA-256: ${identity.sha256}`); + if (artifactResult.artifact.kind === "npm-pack") { + console.log(`npm integrity: ${identity.npmIntegrity}`); + console.log(`npm shasum: ${identity.npmShasum}`); + } + } catch (error) { + spinner?.fail(formatError(error)); + throw error; + } +} + +export async function cmdVerifyPackage( + opts: GlobalOpts, + filePath: string, + options: PackageVerifyOptions = {}, +) { + const targetFile = resolve(opts.workdir, filePath); + if (options.version && options.tag) fail("Use either --version or --tag"); + if ((options.version || options.tag) && !options.packageName?.trim()) { + fail("--package is required with --version or --tag"); + } + + const spinner = options.json ? null : createSpinner("Reading artifact"); + try { + const bytes = new Uint8Array(await readFile(targetFile)); + const identity = computeArtifactIdentity(bytes); + let artifactResult: PackageArtifactResponse | null = null; + + if (options.packageName?.trim()) { + const packageName = normalizePackageNameOrFail(options.packageName); + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + spinnerText(spinner, `Resolving ${packageName}`); + const targetVersion = await resolvePackageVersion(registry, packageName, { + token, + version: options.version, + tag: options.tag, + }); + artifactResult = await apiRequestPackageArtifact(registry, packageName, targetVersion, token); + validateDownloadedArtifact(packageName, artifactResult, bytes, identity); + } + + const expectedSha256 = + options.sha256?.trim() || + (artifactResult?.artifact.kind === "npm-pack" ? artifactResult.artifact.sha256 : undefined); + const expectedNpmIntegrity = + options.npmIntegrity?.trim() || artifactResult?.artifact.npmIntegrity; + const expectedNpmShasum = options.npmShasum?.trim() || artifactResult?.artifact.npmShasum; + assertDigestMatch("SHA-256", expectedSha256, identity.sha256); + assertDigestMatch("npm integrity", expectedNpmIntegrity, identity.npmIntegrity); + assertDigestMatch("npm shasum", expectedNpmShasum, identity.npmShasum); + + spinner?.stop(); + const output = { + path: targetFile, + bytes: bytes.byteLength, + sha256: identity.sha256, + npmIntegrity: identity.npmIntegrity, + npmShasum: identity.npmShasum, + expected: { + sha256: expectedSha256, + npmIntegrity: expectedNpmIntegrity, + npmShasum: expectedNpmShasum, + package: artifactResult?.package.name, + version: artifactResult?.version, + artifactKind: artifactResult?.artifact.kind, + }, + verified: Boolean(expectedSha256 || expectedNpmIntegrity || expectedNpmShasum), + }; + if (options.json) { + process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); + return; + } + console.log(`Path: ${targetFile}`); + console.log(`SHA-256: ${identity.sha256}`); + console.log(`npm integrity: ${identity.npmIntegrity}`); + console.log(`npm shasum: ${identity.npmShasum}`); + if (output.verified) { + console.log("OK. Artifact verification passed."); + } else { + console.log("Computed artifact digests. Pass --package or expected digests to verify."); + } + } catch (error) { + spinner?.fail(formatError(error)); + throw error; + } +} + +export async function cmdDeletePackage( + opts: GlobalOpts, + nameArg: string, + options: PackageDeleteOptions = {}, + inputAllowed = true, +) { + const name = nameArg.trim(); + if (!name) fail("Package name required"); + + if (!options.yes) { + if (!isInteractive() || inputAllowed === false) fail("Pass --yes (no input)"); + const ok = await promptConfirm(`Delete ${name}? (soft delete package and all releases)`); + if (!ok) return undefined; + } + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Deleting ${name}`); + try { + const result = await apiRequest( + registry, + { + method: "DELETE", + path: `${ApiRoutes.packages}/${encodeURIComponent(name)}`, + token, + }, + ApiV1DeleteResponseSchema, + ); + spinner.succeed(`OK. Deleted ${name}`); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + } + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdUndeletePackage( + opts: GlobalOpts, + nameArg: string, + options: PackageUndeleteOptions = {}, + inputAllowed = true, +) { + const name = nameArg.trim(); + if (!name) fail("Package name required"); + + if (!options.yes) { + if (!isInteractive() || inputAllowed === false) fail("Pass --yes (no input)"); + const ok = await promptConfirm(`Restore ${name}? (restore package and releases)`); + if (!ok) return undefined; + } + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Restoring ${name}`); + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/undelete`, + token, + }, + ApiV1DeleteResponseSchema, + ); + spinner.succeed(`OK. Restored ${name}`); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + } + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdTransferPackage( + opts: GlobalOpts, + nameArg: string, + options: PackageTransferOptions, +) { + const name = normalizePackageNameOrFail(nameArg); + const toOwner = options.to?.trim().replace(/^@+/, "").toLowerCase(); + if (!toOwner) fail("--to required"); + const reason = options.reason?.trim(); + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Transferring ${name} to @${toOwner}`); + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/transfer`, + token, + body: { + toOwner, + ...(reason ? { reason } : {}), + }, + }, + ApiV1PackageTransferResponseSchema, + ); + spinner.succeed(`OK. Transferred ${name} to @${toOwner}`); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + } + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdReportPackage( + opts: GlobalOpts, + packageName: string, + options: PackageReportOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + const reason = options.reason?.trim(); + const version = options.version?.trim(); + if (!reason) fail("--reason required"); + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = options.json ? null : createSpinner(`Reporting ${trimmed}`); + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/report`, + token, + body: { + reason, + ...(version ? { version } : {}), + }, + }, + ApiV1PackageReportResponseSchema, + ); + spinner?.stop(); + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + if (result.alreadyReported) { + console.log(`Already reported ${trimmed}.`); + return; + } + const versionSuffix = version ? `@${version}` : ""; + console.log(`OK. Reported ${trimmed}${versionSuffix} for moderator review.`); + } catch (error) { + spinner?.fail(formatError(error)); + throw error; + } +} + +export async function cmdPackageModerationStatus( + opts: GlobalOpts, + packageName: string, + options: PackageModerationStatusOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const result = await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/moderation`, + token, + }, + ApiV1PackageModerationStatusResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + + console.log(`${result.package.name} moderation`); + console.log(` package scan: ${result.package.scanStatus ?? "unknown"}`); + console.log(` open reports: ${result.package.reportCount}`); + if (!result.latestRelease) { + console.log(" latest release: none"); + return; + } + const state = result.latestRelease.moderationState ?? "none"; + console.log(` latest: ${result.latestRelease.version}`); + console.log(` release scan: ${result.latestRelease.scanStatus}`); + console.log(` manual state: ${state}`); + console.log(` blocked: ${result.latestRelease.blockedFromDownload ? "yes" : "no"}`); + if (result.latestRelease.reasons.length > 0) { + console.log(` reasons: ${result.latestRelease.reasons.join(", ")}`); + } + if (result.latestRelease.moderationReason) { + console.log(` note: ${result.latestRelease.moderationReason}`); + } +} + +export async function cmdPackageReadiness( + opts: GlobalOpts, + packageName: string, + options: PackageReadinessOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const result = await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/readiness`, + token, + }, + ApiV1PackageReadinessResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + + console.log(`${result.package.name} readiness: ${result.ready ? "ready" : "blocked"}`); + for (const check of result.checks) { + console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`); + } + if (result.blockers.length > 0) { + console.log(`Blockers: ${result.blockers.join(", ")}`); + } +} + +export async function cmdPackageMigrationStatus( + opts: GlobalOpts, + packageName: string, + options: PackageMigrationStatusOptions = {}, +) { + const trimmed = normalizePackageNameOrFail(packageName); + const token = await getOptionalAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const result = await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/readiness`, + token, + }, + ApiV1PackageReadinessResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + + const version = result.package.latestVersion ?? "no release"; + console.log(`${result.package.name} migration: ${result.ready ? "ready" : "blocked"}`); + console.log(`Version: ${version}`); + console.log(`Official: ${result.package.isOfficial ? "yes" : "no"}`); + for (const check of result.checks) { + console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`); + } + if (result.blockers.length > 0) { + console.log(`Blockers: ${result.blockers.join(", ")}`); + } +} + +async function apiRequestPackageDetail(registry: string, name: string, token?: string) { + return await apiRequest( + registry, + { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(name)}`, token }, + ApiV1PackageResponseSchema, + ); +} + +async function apiRequestPackageArtifact( + registry: string, + name: string, + version: string, + token?: string, +) { + return await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}/artifact`, + token, + }, + ApiV1PackageArtifactResponseSchema, + ); +} + +async function apiRequestPackageTrustedPublisher(registry: string, name: string, token?: string) { + return await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/trusted-publisher`, + token, + }, + ApiV1PackageTrustedPublisherResponseSchema, + ); +} + +async function apiRequestPackageVersion( + registry: string, + name: string, + version: string, + token?: string, +) { + return await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`, + token, + }, + ApiV1PackageVersionResponseSchema, + ); +} + +async function apiRequestPackageVersions( + registry: string, + name: string, + limit: number, + token?: string, +) { + const url = registryUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/versions`, registry); + url.searchParams.set("limit", String(limit)); + return await apiRequest( + registry, + { method: "GET", url: url.toString(), token }, + ApiV1PackageVersionListResponseSchema, + ); +} + +async function resolvePackageVersion( + registry: string, + name: string, + args: { token?: string; version?: string; tag?: string }, +) { + if (args.version?.trim()) return args.version.trim(); + const detail = await apiRequestPackageDetail(registry, name, args.token); + if (!detail.package) fail("Package not found"); + const tags = normalizeTags(detail.package.tags); + if (args.tag?.trim()) { + const tagged = tags[args.tag.trim()]; + if (!tagged) fail(`Unknown tag "${args.tag.trim()}"`); + return tagged; + } + const latest = detail.package.latestVersion ?? tags.latest; + if (!latest) fail("Could not resolve latest version"); + return latest; +} + +function normalizePackageNameOrFail(raw: string) { + const trimmed = raw.trim(); + if (!trimmed) fail("Package name required"); + return trimmed; +} + +function spinnerText(spinner: ReturnType | null, text: string) { + if (spinner) spinner.text = text; +} + +function clampLimit(value: number, max: number) { + if (!Number.isFinite(value)) return Math.min(25, max); + return Math.max(1, Math.min(Math.round(value), max)); +} + +function formatPackageLine(item: { + name: string; + displayName: string; + family: PackageFamily; + latestVersion?: string | null; + channel: "official" | "community" | "private"; + isOfficial: boolean; + verificationTier?: string | null; + summary?: string | null; +}) { + const flags = [ + familyLabel(item.family), + item.isOfficial ? "official" : item.channel, + item.verificationTier ?? null, + ].filter(Boolean); + const version = item.latestVersion ? ` v${item.latestVersion}` : ""; + const summary = item.summary ? ` ${item.summary}` : ""; + return `${item.name}${version} ${item.displayName} [${flags.join(", ")}]${summary}`; +} + +function computeArtifactIdentity(bytes: Uint8Array): ArtifactIdentity { + return { + sha256: digestHex(bytes, "sha256"), + npmIntegrity: `sha512-${digestBase64(bytes, "sha512")}`, + npmShasum: digestHex(bytes, "sha1"), + byteLength: bytes.byteLength, + }; +} + +function digestHex(bytes: Uint8Array, algorithm: "sha1" | "sha256") { + return createHash(algorithm).update(bytes).digest("hex"); +} + +function digestBase64(bytes: Uint8Array, algorithm: "sha512") { + return createHash(algorithm).update(bytes).digest("base64"); +} + +function validateDownloadedArtifact( + requestedPackageName: string, + artifactResult: PackageArtifactResponse, + bytes: Uint8Array, + identity: ArtifactIdentity, +) { + const artifact = artifactResult.artifact; + if (artifact.kind === "npm-pack") { + assertDigestMatch("SHA-256", artifact.sha256, identity.sha256); + if (typeof artifact.size === "number" && artifact.size !== identity.byteLength) { + fail(`artifact size mismatch: expected ${artifact.size}, got ${identity.byteLength}`); + } + assertDigestMatch("npm integrity", artifact.npmIntegrity, identity.npmIntegrity); + assertDigestMatch("npm shasum", artifact.npmShasum, identity.npmShasum); + const parsed = parseClawPack(bytes); + if (parsed.packageName !== artifactResult.package.name) { + fail( + `ClawPack package name mismatch: expected ${artifactResult.package.name}, got ${parsed.packageName}`, + ); + } + if (parsed.packageVersion !== artifactResult.version) { + fail( + `ClawPack package version mismatch: expected ${artifactResult.version}, got ${parsed.packageVersion}`, + ); + } + if (requestedPackageName !== artifactResult.package.name) { + fail( + `Resolved package mismatch: expected ${requestedPackageName}, got ${artifactResult.package.name}`, + ); + } + } + if (requestedPackageName !== artifactResult.package.name) { + fail( + `Resolved package mismatch: expected ${requestedPackageName}, got ${artifactResult.package.name}`, + ); + } +} + +function assertDigestMatch(label: string, expected: string | null | undefined, actual: string) { + if (!expected) return; + if (expected !== actual) { + fail(`${label} mismatch: expected ${expected}, got ${actual}`); + } +} + +function defaultArtifactFilename( + name: string, + version: string, + artifact: PackageArtifactResponse["artifact"], +) { + if (artifact.kind === "npm-pack" && artifact.npmTarballName) return artifact.npmTarballName; + const safeName = name + .replace(/^@/, "") + .replaceAll("/", "-") + .replace(/[^a-zA-Z0-9._-]/g, "-"); + return `${safeName}-${version}.${artifact.kind === "npm-pack" ? "tgz" : "zip"}`; +} + +async function resolveArtifactOutputPath( + opts: GlobalOpts, + output: string | undefined, + filename: string, +) { + if (!output?.trim()) return resolve(opts.workdir, filename); + const resolved = resolve(opts.workdir, output.trim()); + const outputStat = await stat(resolved).catch(() => null); + if (outputStat?.isDirectory()) return join(resolved, filename); + return resolved; +} + +async function assertOutputWritable(path: string, force: boolean) { + const existing = await stat(path).catch(() => null); + if (existing && !force) fail(`Refusing to overwrite ${path}. Use --force.`); + await mkdir(dirname(path), { recursive: true }); +} + +function printPackageSummary(detail: PackageResponse) { + if (!detail.package) return; + const pkg = detail.package; + console.log(`${pkg.name} ${pkg.displayName}`); + console.log(`Family: ${familyLabel(pkg.family)}`); + console.log(`Channel: ${pkg.channel}${pkg.isOfficial ? " (official)" : ""}`); + if (pkg.summary) console.log(`Summary: ${pkg.summary}`); + if (pkg.runtimeId) console.log(`Runtime ID: ${pkg.runtimeId}`); + if (detail.owner?.handle || detail.owner?.displayName) { + console.log(`Owner: ${detail.owner.handle ?? detail.owner.displayName}`); + } + console.log(`Created: ${formatTimestamp(pkg.createdAt)}`); + console.log(`Updated: ${formatTimestamp(pkg.updatedAt)}`); + if (pkg.latestVersion) console.log(`Latest: ${pkg.latestVersion}`); + printArtifact(pkg.artifact ?? null); + const tags = Object.entries(normalizeTags(pkg.tags)); + if (tags.length > 0) { + console.log(`Tags: ${tags.map(([tag, version]) => `${tag}=${version}`).join(", ")}`); + } +} + +function printVersionSummary(version: NonNullable) { + console.log(`Selected: ${version.version}`); + console.log(`Selected At: ${formatTimestamp(version.createdAt)}`); + if (version.changelog.trim()) console.log(`Changelog: ${truncate(version.changelog, 120)}`); +} + +function printTrustedPublisher(trustedPublisher: PackageTrustedPublisher) { + console.log(`Provider: ${trustedPublisher.provider}`); + console.log(`Repository: ${trustedPublisher.repository}`); + console.log(`Workflow: ${trustedPublisher.workflowFilename}`); + if (trustedPublisher.environment) { + console.log(`Environment: ${trustedPublisher.environment}`); + } +} + +function printCompatibility(compatibility: PackageCompatibility | null | undefined) { + if (!compatibility) return; + const entries = formatCompatibilityEntries(compatibility); + if (entries.length > 0) console.log(`Compatibility: ${entries.join(", ")}`); +} + +function formatCompatibilityEntries(compatibility: PackageCompatibility) { + return [ + compatibility.pluginApiRange ? `pluginApi=${compatibility.pluginApiRange}` : null, + compatibility.builtWithOpenClawVersion + ? `builtWith=${compatibility.builtWithOpenClawVersion}` + : null, + compatibility.pluginSdkVersion ? `sdk=${compatibility.pluginSdkVersion}` : null, + compatibility.minGatewayVersion ? `minGateway=${compatibility.minGatewayVersion}` : null, + ].filter(Boolean); +} + +function printCapabilities(capabilities: PackageCapabilitySummary | null | undefined) { + if (!capabilities) return; + console.log(`Executes code: ${capabilities.executesCode ? "yes" : "no"}`); + if (capabilities.pluginKind) console.log(`Plugin kind: ${capabilities.pluginKind}`); + if (capabilities.bundleFormat) console.log(`Bundle format: ${capabilities.bundleFormat}`); + if (capabilities.hostTargets?.length) { + console.log(`Host targets: ${capabilities.hostTargets.join(", ")}`); + } + if (capabilities.channels?.length) console.log(`Channels: ${capabilities.channels.join(", ")}`); + if (capabilities.providers?.length) { + console.log(`Providers: ${capabilities.providers.join(", ")}`); + } + if (capabilities.toolNames?.length) console.log(`Tools: ${capabilities.toolNames.join(", ")}`); + if (capabilities.commandNames?.length) { + console.log(`Commands: ${capabilities.commandNames.join(", ")}`); + } + if (capabilities.serviceNames?.length) { + console.log(`Services: ${capabilities.serviceNames.join(", ")}`); + } +} + +function printVerification(verification: PackageVerificationSummary | null | undefined) { + if (!verification) return; + console.log(`Verification: ${verification.tier} / ${verification.scope}`); + if (verification.summary) console.log(`Verification Summary: ${verification.summary}`); + if (verification.sourceRepo) console.log(`Source Repo: ${verification.sourceRepo}`); + if (verification.sourceCommit) console.log(`Source Commit: ${verification.sourceCommit}`); + if (verification.sourceTag) console.log(`Source Ref: ${verification.sourceTag}`); + if (verification.scanStatus) console.log(`Scan: ${verification.scanStatus}`); +} + +function printArtifact(artifact: PackageArtifactSummary | null | undefined) { + if (!artifact || typeof artifact !== "object") return; + const summary = artifact as { + kind?: string; + sha256?: string; + size?: number; + format?: string; + npmIntegrity?: string; + npmShasum?: string; + npmTarballName?: string; + }; + if (!summary.kind) return; + console.log(`Artifact: ${summary.kind}${summary.format ? ` (${summary.format})` : ""}`); + if (summary.sha256) console.log(`Artifact SHA-256: ${summary.sha256}`); + if (typeof summary.size === "number") { + console.log(`Artifact Size: ${formatByteCount(summary.size)}`); + } + if (summary.npmIntegrity) console.log(`npm integrity: ${summary.npmIntegrity}`); + if (summary.npmShasum) console.log(`npm shasum: ${summary.npmShasum}`); + if (summary.npmTarballName) console.log(`npm tarball: ${summary.npmTarballName}`); +} + +function normalizeTags(tags: unknown): Record { + if (!tags || typeof tags !== "object") return {}; + const resolved: Record = {}; + for (const [tag, version] of Object.entries(tags as Record)) { + if (typeof version === "string") resolved[tag] = version; + } + return resolved; +} + +function normalizeFiles(files: unknown): PrintableFile[] { + if (!Array.isArray(files)) return []; + return files + .map((file) => { + if (!file || typeof file !== "object") return null; + const entry = file as { + path?: unknown; + size?: unknown; + sha256?: unknown; + contentType?: unknown; + }; + if (typeof entry.path !== "string") return null; + return { + path: entry.path, + size: typeof entry.size === "number" ? entry.size : null, + sha256: typeof entry.sha256 === "string" ? entry.sha256 : null, + contentType: typeof entry.contentType === "string" ? entry.contentType : null, + }; + }) + .filter((entry): entry is PrintableFile => Boolean(entry)); +} + +function formatFileLine(file: PrintableFile) { + const size = typeof file.size === "number" ? `${file.size}B` : "?"; + const hash = file.sha256 ?? "?"; + return `- ${file.path} ${size} ${hash}`; +} + +function familyLabel(family: PackageFamily) { + switch (family) { + case "code-plugin": + return "Code Plugin"; + case "bundle-plugin": + return "Bundle Plugin"; + default: + return "Skill"; + } +} + +function truncate(value: string, max: number) { + return value.length <= max ? value : `${value.slice(0, max - 1)}…`; +} + +function formatTimestamp(value: number) { + return new Date(value).toISOString(); +} + +async function readJsonFile(path: string) { + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as unknown; + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : null; + } catch { + return null; + } +} + +function packageJsonString(value: Record | null, key: string): string | undefined { + const candidate = value?.[key]; + return typeof candidate === "string" && candidate.trim() ? candidate.trim() : undefined; +} + +function assertClawPackSize(size: number, label: string) { + if (size > MAX_CLAWPACK_BYTES) { + fail(`ClawPack "${label}" exceeds 120MB limit`); + } +} + +const REAL_BUNDLE_MANIFESTS = [ + { path: ".codex-plugin/plugin.json", format: "codex" }, + { path: ".claude-plugin/plugin.json", format: "claude" }, + { path: ".cursor-plugin/plugin.json", format: "cursor" }, +] as const; + +function hasRealBundleMarker(fileSet: Set) { + return ( + REAL_BUNDLE_MANIFESTS.some((marker) => fileSet.has(marker.path)) || + Array.from(fileSet).some( + (path) => + path.startsWith("skills/") || + path.startsWith("commands/") || + path.startsWith("agents/") || + path === "hooks/hooks.json" || + path === ".mcp.json" || + path === ".lsp.json" || + path === "settings.json", + ) + ); +} + +function detectPackageFamily( + fileSet: Set, + explicit?: "code-plugin" | "bundle-plugin", +): "code-plugin" | "bundle-plugin" { + if (explicit) return explicit; + if (hasRealBundleMarker(fileSet)) return "bundle-plugin"; + if (fileSet.has("openclaw.plugin.json")) return "code-plugin"; + return fail("Could not detect package family. Use --family."); +} + +async function readBundleManifestInfo( + filesOnDisk: PackageFile[], + folder: string, + parsedClawpack: ReturnType | undefined, +) { + for (const marker of REAL_BUNDLE_MANIFESTS) { + const manifest = + readJsonEntry(filesOnDisk, marker.path) ?? + (parsedClawpack ? null : await readJsonFile(join(folder, marker.path))); + if (manifest) return { manifest, format: marker.format }; + } + return { manifest: null, format: undefined }; +} + +function parseTags(value: string) { + return value + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); +} + +function parseCsv(value: string | undefined) { + if (!value) return []; + return value + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean); +} + +function applyGitHubSourcePath( + source: Awaited>, + sourcePath: string | undefined, +) { + const explicitPath = sourcePath?.trim(); + if (!explicitPath || source.kind !== "github") return source; + return { ...source, path: explicitPath }; +} + +async function preparePackagePublishPlan( + opts: GlobalOpts, + sourceArg: string, + options: PackagePublishOptions, +): Promise { + const resolvedSource = await resolveSourceInput(sourceArg, { + workdir: opts.workdir, + localWorkdirs: [process.cwd(), opts.workdir], + }); + const sourceForFetch = applyGitHubSourcePath(resolvedSource, options.sourcePath); + let folder = sourceForFetch.kind === "local" ? sourceForFetch.path : ""; + let cleanup: (() => Promise) | undefined; + let inferredSource: InferredPublishSource | undefined; + let clawpackOnDisk: PackageFile | undefined; + let parsedClawpack: ReturnType | undefined; + const addCleanup = (next: () => Promise) => { + const previous = cleanup; + cleanup = async () => { + await next(); + await previous?.(); + }; + }; + + if (sourceForFetch.kind === "github") { + const fetchSpinner = options.json + ? null + : createSpinner(`Fetching ${sourceForFetch.owner}/${sourceForFetch.repo}`); + try { + const fetched = await fetchGitHubSource(sourceForFetch); + folder = fetched.dir; + cleanup = fetched.cleanup; + inferredSource = fetched.source; + fetchSpinner?.stop(); + } catch (error) { + fetchSpinner?.fail(formatError(error)); + throw error; + } + } else { + const folderStat = await stat(folder).catch(() => null); + if (!folderStat) fail("Path must be a folder or ClawPack .tgz"); + if (folderStat.isFile()) { + if (!folder.endsWith(".tgz")) fail("ClawPack publish files must end in .tgz"); + const bytes = new Uint8Array(await readFile(folder)); + assertClawPackSize(bytes.byteLength, basename(folder)); + parsedClawpack = parseClawPack(bytes); + clawpackOnDisk = { + relPath: basename(folder), + bytes, + contentType: "application/octet-stream", + }; + } else if (!folderStat.isDirectory()) { + fail("Path must be a folder or ClawPack .tgz"); + } + + const localGitInfo = folderStat.isDirectory() ? resolveLocalGitInfo(folder) : null; + if (localGitInfo) { + inferredSource = { + repo: localGitInfo.repo, + commit: localGitInfo.commit, + ref: localGitInfo.ref, + path: localGitInfo.path, + ...(localGitInfo.repo ? { url: `https://github.com/${localGitInfo.repo}` } : {}), + }; + } + } + + let filesOnDisk = parsedClawpack + ? parsedClawpack.entries.map((entry) => ({ + relPath: entry.path, + bytes: entry.bytes, + contentType: mime.getType(entry.path) ?? "application/octet-stream", + })) + : await listPackageFiles(folder); + if (filesOnDisk.length === 0) fail("No files found"); + + const fileSet = new Set(filesOnDisk.map((file) => file.relPath.toLowerCase())); + const packageJson = + parsedClawpack?.packageJson ?? (await readJsonFile(join(folder, "package.json"))); + const pluginManifest = + readJsonEntry(filesOnDisk, "openclaw.plugin.json") ?? + (parsedClawpack ? null : await readJsonFile(join(folder, "openclaw.plugin.json"))); + const bundleManifestInfo = await readBundleManifestInfo(filesOnDisk, folder, parsedClawpack); + const bundleManifest = bundleManifestInfo.manifest; + const family = detectPackageFamily(fileSet, options.family); + const name = + options.name?.trim() || + parsedClawpack?.packageName || + packageJsonString(packageJson, "name") || + packageJsonString(pluginManifest, "id") || + packageJsonString(bundleManifest, "id") || + basename(folder).trim().toLowerCase(); + const displayName = + options.displayName?.trim() || + packageJsonString(packageJson, "displayName") || + packageJsonString(pluginManifest, "name") || + packageJsonString(bundleManifest, "name") || + titleCase(basename(folder)); + const ownerHandle = options.owner?.trim().replace(/^@+/, ""); + const version = + options.version?.trim() || + parsedClawpack?.packageVersion || + packageJsonString(packageJson, "version"); + const changelog = options.changelog ?? ""; + let clawScanNote: string | undefined; + try { + clawScanNote = normalizeClawScanNote(options.clawscanNote); + } catch (error) { + fail(formatError(error)); + } + const tags = parseTags(options.tags ?? "latest"); + const source = buildSource(options, inferredSource); + + if (!name) fail("--name required"); + if (!displayName) fail("--display-name required"); + if (!version) fail("--version required"); + if (!fileSet.has("openclaw.plugin.json")) fail("openclaw.plugin.json required"); + if (family === "code-plugin" && !semver.valid(version)) { + fail("--version must be valid semver for code plugins"); + } + if (family === "code-plugin") { + if (!fileSet.has("package.json")) fail("package.json required"); + if (!source) fail("--source-repo and --source-commit required for code plugins"); + const validation = validateOpenClawExternalCodePluginPackageJson(packageJson); + if (validation.issues.length > 0) { + fail(validation.issues.map((issue) => issue.message).join(" ")); + } + } + + if (family === "code-plugin" && !clawpackOnDisk) { + const packDestination = await mkdtemp(join(tmpdir(), "clawhub-clawpack-")); + let packed: PackedClawPack; + try { + packed = await createClawPackFromFolder({ + sourcePath: folder, + packDestination, + cwd: opts.workdir, + }); + if (packed.parsed.packageName !== name) { + fail(`ClawPack package name mismatch: expected ${name}, got ${packed.parsed.packageName}`); + } + if (packed.parsed.packageVersion !== version) { + fail( + `ClawPack package version mismatch: expected ${version}, got ${packed.parsed.packageVersion}`, + ); + } + } catch (error) { + await rm(packDestination, { recursive: true, force: true }); + throw error; + } + addCleanup(async () => { + await rm(packDestination, { recursive: true, force: true }); + }); + parsedClawpack = packed.parsed; + clawpackOnDisk = packed.file; + filesOnDisk = packed.parsed.entries.map((entry) => ({ + relPath: entry.path, + bytes: entry.bytes, + contentType: mime.getType(entry.path) ?? "application/octet-stream", + })); + } + + const payload: PackagePublishPayload = { + name, + displayName, + ...(ownerHandle ? { ownerHandle } : {}), + family, + version, + changelog, + ...(clawScanNote ? { clawScanNote } : {}), + ...(options.manualOverrideReason?.trim() + ? { manualOverrideReason: options.manualOverrideReason.trim() } + : {}), + tags, + ...(source ? { source } : {}), + ...(family === "bundle-plugin" + ? { + bundle: { + format: options.bundleFormat?.trim() || bundleManifestInfo.format, + hostTargets: parseCsv(options.hostTargets), + }, + } + : {}), + }; + const sourceLabel = describePublishSource(sourceForFetch, source, folder); + + return { + folder, + cleanup, + filesOnDisk, + clawpackOnDisk, + packageJson, + payload, + compatibility: + family === "code-plugin" + ? normalizeOpenClawExternalPluginCompatibility(packageJson) + : undefined, + sourceLabel, + output: { + source: sourceLabel, + name, + displayName, + family, + version, + ...(source?.commit ? { commit: source.commit } : {}), + files: filesOnDisk.length, + totalBytes: clawpackOnDisk + ? clawpackOnDisk.bytes.byteLength + : filesOnDisk.reduce((sum, file) => sum + file.bytes.byteLength, 0), + }, + }; +} + +function readJsonEntry(files: PackageFile[], path: string) { + const file = files.find((entry) => entry.relPath.toLowerCase() === path.toLowerCase()); + if (!file) return null; + try { + const parsed = JSON.parse(new TextDecoder().decode(file.bytes)) as unknown; + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? (parsed as Record) + : null; + } catch { + return null; + } +} + +function hasGitHubActionsOidcEnv(env: NodeJS.ProcessEnv = process.env) { + return Boolean(env.ACTIONS_ID_TOKEN_REQUEST_URL && env.ACTIONS_ID_TOKEN_REQUEST_TOKEN); +} + +async function requestGitHubActionsOidcToken( + audience: string, + options: { + env?: NodeJS.ProcessEnv; + fetchImpl?: typeof fetch; + } = {}, +) { + const env = options.env ?? process.env; + const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis); + const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL?.trim(); + const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN?.trim(); + if (!requestUrl || !requestToken) { + throw new Error("GitHub Actions OIDC is not available in this environment."); + } + + const url = new URL(requestUrl); + url.searchParams.set("audience", audience); + const response = await fetchImpl(url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${requestToken}`, + }, + }); + const responseText = await response.text(); + if (!response.ok) { + throw new Error( + `GitHub OIDC token request failed (${response.status}): ${responseText || response.statusText}`, + ); + } + + let parsed: unknown; + try { + parsed = JSON.parse(responseText); + } catch { + throw new Error("GitHub OIDC token request returned invalid JSON."); + } + + const token = (parsed as { value?: unknown }).value; + if (typeof token !== "string" || !token.trim()) { + throw new Error("GitHub OIDC token response did not include a token value."); + } + return token; +} + +async function mintPackagePublishToken( + registry: string, + packageName: string, + version: string, + githubOidcToken: string, +) { + const response = await apiRequest( + registry, + { + method: "POST", + path: ApiRoutes.publishTokenMint, + body: { + packageName, + version, + githubOidcToken, + }, + }, + ApiV1PublishTokenMintResponseSchema, + ); + return response.token; +} + +async function resolvePackagePublishToken(params: { + registry: string; + packageName: string; + version: string; + manualOverrideReason?: string; + spinner: ReturnType | null; +}) { + if (params.manualOverrideReason?.trim()) { + return await requireAuthToken(); + } + + if (!hasGitHubActionsOidcEnv()) { + return await requireAuthToken(); + } + + if (params.spinner) { + params.spinner.text = "Requesting GitHub Actions OIDC token"; + } + try { + const githubOidcToken = await requestGitHubActionsOidcToken("clawhub"); + if (params.spinner) { + params.spinner.text = "Minting short-lived ClawHub publish token"; + } + return await mintPackagePublishToken( + params.registry, + params.packageName, + params.version, + githubOidcToken, + ); + } catch (error) { + const status = + typeof error === "object" && error !== null && "status" in error + ? (error as { status?: unknown }).status + : undefined; + if (status !== undefined && status !== 400 && status !== 403 && status !== 404) { + throw error; + } + if (params.spinner) { + params.spinner.text = "Trusted publishing unavailable, falling back to ClawHub token"; + } + return await requireAuthToken(); + } +} + +function buildSource(options: PackagePublishOptions, inferred?: InferredPublishSource) { + const rawRepo = options.sourceRepo?.trim() || inferred?.repo?.trim(); + const rawCommit = options.sourceCommit?.trim() || inferred?.commit?.trim(); + const rawRef = options.sourceRef?.trim() || inferred?.ref?.trim(); + const explicitPath = options.sourcePath?.trim(); + const rawPath = explicitPath !== undefined ? explicitPath : inferred?.path?.trim(); + if (!rawRepo && !rawCommit && !rawRef && !rawPath) return undefined; + if (!rawRepo || !rawCommit) fail("--source-repo and --source-commit must be set together"); + const repo = normalizeGitHubRepo(rawRepo); + if (!repo) fail("--source-repo must be a GitHub repo or URL"); + const explicitRepo = options.sourceRepo?.trim(); + const url = explicitRepo + ? explicitRepo.startsWith("http") + ? explicitRepo + : `https://github.com/${repo}` + : inferred?.url || `https://github.com/${repo}`; + return { + kind: "github" as const, + url, + repo, + ref: rawRef || rawCommit, + commit: rawCommit, + path: rawPath || ".", + importedAt: Date.now(), + }; +} + +function describePublishSource( + sourceInput: Awaited>, + source: ReturnType, + folder: string, +) { + if (source) { + return `github:${source.repo}@${source.ref}${source.path !== "." ? `:${source.path}` : ""}`; + } + if (sourceInput.kind === "github") { + const repo = `${sourceInput.owner}/${sourceInput.repo}`; + return `github:${repo}@${sourceInput.ref ?? "HEAD"}${ + sourceInput.path !== "." ? `:${sourceInput.path}` : "" + }`; + } + return `local:${folder}`; +} + +function printPackageDryRun(params: { + source: string; + family: PackageFamily; + name: string; + displayName: string; + version: string; + commit?: string; + compatibility?: PackageCompatibility; + tags: string[]; + files: PackageFile[]; +}) { + console.log("Dry run - nothing will be published."); + console.log(""); + console.log(`Source: ${params.source}`); + console.log(`Family: ${params.family}`); + console.log(`Name: ${params.name}`); + console.log(`Display: ${params.displayName}`); + console.log(`Version: ${params.version}`); + if (params.commit) console.log(`Commit: ${params.commit}`); + if (params.compatibility) { + console.log(`Compat: ${formatCompatibilityEntries(params.compatibility).join(", ")}`); + } + console.log( + `Files: ${params.files.length} files (${formatByteCount( + params.files.reduce((sum, file) => sum + file.bytes.byteLength, 0), + )})`, + ); + console.log(`Tags: ${params.tags.join(", ")}`); + console.log(""); + console.log("Files:"); + for (const file of params.files) { + console.log(` ${file.relPath.padEnd(28)} ${formatByteCount(file.bytes.byteLength)}`); + } +} + +function formatByteCount(value: number) { + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / (1024 * 1024)).toFixed(1)} MB`; +} + +async function listPackageFiles(root: string) { + const files: PackageFile[] = []; + const absRoot = resolve(root); + const ig = ignore(); + ig.add([".git/", "node_modules/", `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`]); + await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); + await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE)); + await walk(absRoot, async (absPath) => { + const relPath = normalizePath(relative(absRoot, absPath)); + if (!relPath || ig.ignores(relPath)) return; + const bytes = new Uint8Array(await readFile(absPath)); + files.push({ + relPath, + bytes, + contentType: mime.getType(relPath) ?? "application/octet-stream", + }); + }); + return files; +} + +function normalizePath(path: string) { + return path + .split(sep) + .join("/") + .replace(/^\.\/+/, ""); +} + +async function walk(dir: string, onFile: (path: string) => Promise) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name === ".git" || entry.name === "node_modules") continue; + const full = join(dir, entry.name); + if (entry.isDirectory()) { + await walk(full, onFile); + continue; + } + if (!entry.isFile()) continue; + await onFile(full); + } +} + +async function addIgnoreFile(ig: ReturnType, path: string) { + try { + const raw = await readFile(path, "utf8"); + ig.add(raw.split(/\r?\n/)); + } catch { + // optional + } +} diff --git a/dt-skill/src/cli/commands/publish.test.ts b/dt-skill/src/cli/commands/publish.test.ts new file mode 100644 index 00000000..6d952e46 --- /dev/null +++ b/dt-skill/src/cli/commands/publish.test.ts @@ -0,0 +1,483 @@ +/* @vitest-environment node */ + +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; +import { MAX_CLAWSCAN_NOTE_CHARS } from "../../schema/index.js"; + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks({ interactive: true }); + +const mockSearchMultiselect = vi.fn(); + +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../ui.js", () => uiMocks.moduleFactory()); +vi.mock("../prompts/search-multiselect.js", async () => { + const actual = await vi.importActual("../prompts/search-multiselect.js"); + return { + ...actual, + searchMultiselect: (opts: any) => mockSearchMultiselect(opts), + }; +}); + +const { cmdPublish } = await import("./publish"); + +async function makeTmpWorkdir() { + const root = await mkdtemp(join(tmpdir(), "clawhub-publish-")); + return root; +} + +function makeOpts(workdir: string) { + return makeGlobalOpts(workdir); +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); +}); + +describe("cmdPublish", () => { + it("publishes SKILL.md from disk (mocked HTTP)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "my-skill"); + await mkdir(folder, { recursive: true }); + const skillContent = "# Skill\n\nHello\n"; + const notesContent = "notes\n"; + await writeFile(join(folder, "SKILL.md"), skillContent, "utf8"); + await writeFile(join(folder, "notes.md"), notesContent, "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: "skill_1", + versionId: "ver_1", + }); + + await cmdPublish(makeOpts(workdir), "my-skill", { + slug: "my-skill", + name: "My Skill", + version: "1.0.0", + changelog: "", + tags: "latest", + clawscanNote: "This skill needs network access to call the user's configured API.", + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/v1/skills"; + }); + if (!publishCall) throw new Error("Missing publish call"); + expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); + expect(publishCall[1]).not.toHaveProperty("token"); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get("payload"); + if (typeof payloadEntry !== "string") throw new Error("Missing publish payload"); + const payload = JSON.parse(payloadEntry); + expect(payload.slug).toBe("my-skill"); + expect(payload.displayName).toBe("My Skill"); + expect(payload.version).toBe("1.0.0"); + expect(payload.changelog).toBe(""); + expect(payload.clawScanNote).toBe( + "This skill needs network access to call the user's configured API.", + ); + expect(payload.acceptLicenseTerms).toBe(true); + expect(payload.tags).toEqual(["latest"]); + const files = publishForm.getAll("files") as Array; + expect(files.map((file) => file.name ?? "").sort()).toEqual(["SKILL.md", "notes.md"]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("rejects oversized clawscan notes before uploading skill files", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "oversized-note"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + + await expect( + cmdPublish(makeOpts(workdir), "oversized-note", { + slug: "oversized-note", + name: "Oversized Note", + version: "1.0.0", + clawscanNote: "x".repeat(MAX_CLAWSCAN_NOTE_CHARS + 1), + }), + ).rejects.toThrow(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("allows empty changelog when updating an existing skill", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "existing-skill"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: "skill_1", + versionId: "ver_2", + }); + + await cmdPublish(makeOpts(workdir), "existing-skill", { + version: "1.0.1", + changelog: "", + tags: "latest", + }); + + expect(httpMocks.apiRequestForm).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ path: "/api/v1/skills", method: "POST" }), + expect.anything(), + ); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("still publishes a root SKILL.md hidden by broad ignore patterns", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "ignored-manifest"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, ".gitignore"), "*.md\n", "utf8"); + await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + await writeFile(join(folder, "notes.md"), "ignored notes\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: "skill_1", + versionId: "ver_1", + }); + + await cmdPublish(makeOpts(workdir), "ignored-manifest", { + slug: "ignored-manifest", + name: "Ignored Manifest", + version: "1.0.0", + changelog: "", + tags: "latest", + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/v1/skills"; + }); + if (!publishCall) throw new Error("Missing publish call"); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const files = publishForm.getAll("files") as Array; + expect(files.map((file) => file.name ?? "")).toEqual(["SKILL.md"]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("includes owner handle for org-owned skill publishes", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "org-skill"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: "skill_1", + versionId: "ver_2", + }); + + await cmdPublish(makeOpts(workdir), "org-skill", { + owner: "@openclaw", + migrateOwner: true, + version: "1.0.1", + changelog: "", + tags: "latest", + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/v1/skills"; + }); + if (!publishCall) throw new Error("Missing publish call"); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get("payload"); + if (typeof payloadEntry !== "string") throw new Error("Missing publish payload"); + const payload = JSON.parse(payloadEntry); + expect(payload.ownerHandle).toBe("openclaw"); + expect(payload.migrateOwner).toBe(true); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it('rejects plugin folders with guidance to use "clawhub package publish"', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "demo-plugin"); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, "package.json"), + JSON.stringify({ name: "demo-plugin", openclaw: { extensions: ["./index.ts"] } }), + "utf8", + ); + await writeFile(join(folder, "openclaw.plugin.json"), '{"id":"demo-plugin"}', "utf8"); + + await expect( + cmdPublish(makeOpts(workdir), "demo-plugin", { + slug: "demo-plugin", + name: "Demo Plugin", + version: "1.0.0", + tags: "latest", + }), + ).rejects.toThrow( + 'This looks like a plugin. Use "clawhub package publish " instead.', + ); + expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + describe("cmdPublish batch mode", () => { + it("detects multiple skill folders and switches to batch upload (T004)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, "skills-batch"); + await mkdir(join(skillsDir, "skill-a"), { recursive: true }); + await mkdir(join(skillsDir, "skill-b"), { recursive: true }); + await writeFile(join(skillsDir, "skill-a", "SKILL.md"), "# Skill A\n", "utf8"); + await writeFile(join(skillsDir, "skill-b", "SKILL.md"), "# Skill B\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: "skill-a", name: "skill-a" }, + { slug: "skill-b", name: "skill-b" }, + ], + }, + }); + + await cmdPublish(makeOpts(workdir), "skills-batch", { + all: true, + tags: "latest", + }); + + // Should call /api/skills/import-file, NOT /api/v1/skills + const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/skills/import-file"; + }); + expect(batchCall).toBeDefined(); + + // Should send packageName derived from folder basename + const form = (batchCall![1] as { form?: FormData }).form as FormData; + const packageName = form.get("packageName"); + expect(packageName).toBe("skills-batch"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("packs selected skills into ZIP with correct structure (T005)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, "zip-test"); + await mkdir(join(skillsDir, "alpha"), { recursive: true }); + await mkdir(join(skillsDir, "beta"), { recursive: true }); + await writeFile(join(skillsDir, "alpha", "SKILL.md"), "# Alpha\n", "utf8"); + await writeFile(join(skillsDir, "alpha", "config.json"), "{\"a\":1}", "utf8"); + await writeFile(join(skillsDir, "beta", "SKILL.md"), "# Beta\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: "alpha", name: "alpha" }, + { slug: "beta", name: "beta" }, + ], + }, + }); + + await cmdPublish(makeOpts(workdir), "zip-test", { all: true }); + + const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/skills/import-file"; + }); + expect(batchCall).toBeDefined(); + const form = (batchCall![1] as { form?: FormData }).form as FormData; + const fileEntry = form.get("file"); + expect(fileEntry).toBeDefined(); + + // Verify ZIP contents + const AdmZip = (await import("adm-zip")).default; + const arrayBuffer = await (fileEntry as Blob).arrayBuffer(); + const zip = new AdmZip(Buffer.from(arrayBuffer)); + const entries = zip.getEntries().map((e: any) => e.entryName); + expect(entries.some((n: string) => n.includes("alpha"))).toBe(true); + expect(entries.some((n: string) => n.includes("beta"))).toBe(true); + expect(entries.some((n: string) => n.includes("SKILL.md"))).toBe(true); + + // ZIP filename should use folder basename + const batchCall2 = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/skills/import-file"; + }); + const form2 = (batchCall2![1] as { form?: FormData }).form as FormData; + const packageName2 = form2.get("packageName"); + expect(packageName2).toBe("zip-test"); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("calls /api/skills/import-file for batch, not /api/v1/skills (T006)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, "api-check"); + await mkdir(join(skillsDir, "x-skill"), { recursive: true }); + await mkdir(join(skillsDir, "y-skill"), { recursive: true }); + await writeFile(join(skillsDir, "x-skill", "SKILL.md"), "# X\n", "utf8"); + await writeFile(join(skillsDir, "y-skill", "SKILL.md"), "# Y\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [], + }, + }); + + await cmdPublish(makeOpts(workdir), "api-check", { all: true }); + + const v1Call = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/v1/skills"; + }); + expect(v1Call).toBeUndefined(); // Should NOT call /api/v1/skills in batch mode + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("uses searchMultiselect in interactive mode and only uploads selected (T011)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, "interactive"); + await mkdir(join(skillsDir, "s1"), { recursive: true }); + await mkdir(join(skillsDir, "s2"), { recursive: true }); + await mkdir(join(skillsDir, "s3"), { recursive: true }); + await writeFile(join(skillsDir, "s1", "SKILL.md"), "# S1\n", "utf8"); + await writeFile(join(skillsDir, "s2", "SKILL.md"), "# S2\n", "utf8"); + await writeFile(join(skillsDir, "s3", "SKILL.md"), "# S3\n", "utf8"); + + // User selects only s1 and s3 + mockSearchMultiselect.mockResolvedValue(["s1", "s3"]); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: "s1", name: "s1" }, + { slug: "s3", name: "s3" }, + ], + }, + }); + + await cmdPublish(makeOpts(workdir), "interactive", { tags: "latest" }); + + expect(mockSearchMultiselect).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("3 found") }), + ); + + const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === "/api/skills/import-file"; + }); + expect(batchCall).toBeDefined(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("reports imported count and skill list on success (T014)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, "report"); + await mkdir(join(skillsDir, "r1"), { recursive: true }); + await mkdir(join(skillsDir, "r2"), { recursive: true }); + await writeFile(join(skillsDir, "r1", "SKILL.md"), "# R1\n", "utf8"); + await writeFile(join(skillsDir, "r2", "SKILL.md"), "# R2\n", "utf8"); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: "r1", name: "r1" }, + { slug: "r2", name: "r2" }, + ], + }, + }); + + const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); + + await cmdPublish(makeOpts(workdir), "report", { all: true }); + + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + expect.stringContaining("2 skill(s)"), + ); + expect(mockLog).toHaveBeenCalledWith( + expect.stringContaining("r1"), + ); + + mockLog.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + + it("shows error on upload failure (T015)", async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, "fail-test"); + await mkdir(join(skillsDir, "f1"), { recursive: true }); + await writeFile(join(skillsDir, "f1", "SKILL.md"), "# F1\n", "utf8"); + + httpMocks.apiRequestForm.mockRejectedValueOnce(new Error("Network error")); + + await expect( + cmdPublish(makeOpts(workdir), "fail-test", { all: true }), + ).rejects.toThrow("Network error"); + + expect(uiMocks.spinner.fail).toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + }); +}); diff --git a/dt-skill/src/cli/commands/publish.ts b/dt-skill/src/cli/commands/publish.ts new file mode 100644 index 00000000..7bb7f613 --- /dev/null +++ b/dt-skill/src/cli/commands/publish.ts @@ -0,0 +1,315 @@ +import { readFile, readdir, stat } from "node:fs/promises"; +import { basename, join, resolve } from "node:path"; +import AdmZip from "adm-zip"; +import semver from "semver"; +import { apiRequestForm } from "../../http.js"; +import { + ApiRoutes, + ApiV1PublishResponseSchema, + normalizeClawScanNote, +} from "../../schema/index.js"; +import { listTextFiles } from "../../skills.js"; +import { getRegistry } from "../registry.js"; +import { sanitizeSlug, titleCase } from "../slug.js"; +import { findSkillFolders } from "../scanSkills.js"; +import { searchMultiselect } from "../prompts/search-multiselect.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive } from "../ui.js"; + +export async function cmdPublish( + opts: GlobalOpts, + folderArg: string, + options: { + slug?: string; + name?: string; + owner?: string; + version?: string; + changelog?: string; + tags?: string; + forkOf?: string; + clawscanNote?: string; + migrateOwner?: boolean; + all?: boolean; + category?: string; + }, +) { + // Resolve folder path: try workdir first (standard behavior), + // but fall back to cwd so relative paths work from whichever directory + // the user runs the command (workdir may point to a clawdbot workspace) + const folder = folderArg + ? await resolveFolderPath(opts.workdir, folderArg) + : null; + if (!folder) fail("Path required"); + const folderStat = await stat(folder).catch(() => null); + if (!folderStat || !folderStat.isDirectory()) fail("Path must be a folder"); + if (await looksLikePluginFolder(folder)) { + fail('This looks like a plugin. Use "clawhub package publish " instead.'); + } + + // Detect batch mode: if folder does NOT contain SKILL.md directly, + // but contains subdirectories with SKILL.md, switch to batch upload + const directSkillMd = await stat(join(folder, "SKILL.md")).catch(() => null); + if (!directSkillMd?.isFile()) { + const skillFolders = await findSkillFolders(folder); + if (skillFolders.length > 0) { + return cmdPublishBatch(opts, folder, skillFolders, options); + } + } + + // Single skill mode (existing logic) + const registry = await getRegistry(opts, { cache: true }); + + const slug = options.slug ?? sanitizeSlug(basename(folder)); + const displayName = options.name ?? titleCase(basename(folder)); + const ownerHandle = options.owner?.trim().replace(/^@+/, ""); + const version = options.version; + const changelog = options.changelog ?? ""; + let clawScanNote: string | undefined; + try { + clawScanNote = normalizeClawScanNote(options.clawscanNote); + } catch (error) { + fail(formatError(error)); + } + const tagsValue = options.tags ?? "latest"; + const tags = tagsValue + .split(",") + .map((tag) => tag.trim()) + .filter(Boolean); + + const forkOfRaw = options.forkOf?.trim(); + const forkOf = forkOfRaw ? parseForkOf(forkOfRaw) : undefined; + + if (!slug) fail("--slug required"); + if (!displayName) fail("--name required"); + if (!version || !semver.valid(version)) fail("--version must be valid semver"); + + const spinner = createSpinner(`Preparing ${slug}@${version}`); + try { + const filesOnDisk = await ensureRootManifestFile(folder, await listTextFiles(folder)); + if (filesOnDisk.length === 0) fail("No files found"); + if ( + !filesOnDisk.some((file) => { + const lower = file.relPath.toLowerCase(); + return lower === "skill.md" || lower === "skills.md"; + }) + ) { + fail("SKILL.md required"); + } + + const form = new FormData(); + form.set( + "payload", + JSON.stringify({ + slug, + displayName, + ...(ownerHandle ? { ownerHandle } : {}), + ...(options.migrateOwner ? { migrateOwner: true } : {}), + version, + changelog, + ...(clawScanNote ? { clawScanNote } : {}), + acceptLicenseTerms: true, + tags, + ...(forkOf ? { forkOf } : {}), + }), + ); + + let index = 0; + for (const file of filesOnDisk) { + index += 1; + spinner.text = `Uploading ${file.relPath} (${index}/${filesOnDisk.length})`; + const blob = new Blob([Buffer.from(file.bytes)], { type: file.contentType ?? "text/plain" }); + form.append("files", blob, file.relPath); + } + + spinner.text = `Publishing ${slug}@${version}`; + const result = await apiRequestForm( + registry, + { method: "POST", path: ApiRoutes.skills, form }, + ApiV1PublishResponseSchema, + ); + + spinner.succeed(`OK. Published ${slug}@${version} (${result.versionId})`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +async function ensureRootManifestFile( + folder: string, + files: Awaited>, +) { + if ( + files.some((file) => { + const lower = file.relPath.toLowerCase(); + return lower === "skill.md" || lower === "skills.md"; + }) + ) { + return files; + } + + const entries = await readdir(folder, { withFileTypes: true }).catch(() => []); + const manifest = entries.find((entry) => { + const lower = entry.name.toLowerCase(); + return entry.isFile() && (lower === "skill.md" || lower === "skills.md"); + }); + if (!manifest) return files; + + return [ + ...files, + { + relPath: manifest.name, + bytes: new Uint8Array(await readFile(join(folder, manifest.name))), + contentType: "text/markdown", + }, + ]; +} + +async function looksLikePluginFolder(folder: string) { + const checks = [ + join(folder, "openclaw.plugin.json"), + join(folder, "package.json"), + join(folder, ".codex-plugin", "plugin.json"), + join(folder, ".claude-plugin", "plugin.json"), + join(folder, ".cursor-plugin", "plugin.json"), + ]; + const stats = await Promise.all(checks.map((candidate) => stat(candidate).catch(() => null))); + if (stats[0]?.isFile() || stats[2]?.isFile() || stats[3]?.isFile() || stats[4]?.isFile()) { + return true; + } + if (!stats[1]?.isFile()) { + return false; + } + try { + const raw = JSON.parse(await readFile(checks[1], "utf8")) as { openclaw?: unknown }; + return Boolean( + raw && typeof raw === "object" && raw.openclaw && typeof raw.openclaw === "object", + ); + } catch { + return false; + } +} + +function parseForkOf(value: string) { + const trimmed = value.trim(); + const [slugRaw, versionRaw] = trimmed.split("@"); + const slug = (slugRaw ?? "").trim().toLowerCase(); + if (!slug) fail("--fork-of must be or "); + const version = (versionRaw ?? "").trim(); + if (version && !semver.valid(version)) fail("--fork-of version must be valid semver"); + return { slug, version: version || undefined }; +} + +export async function cmdPublishBatch( + opts: GlobalOpts, + folder: string, + discoveredSkills: Array<{ folder: string; slug: string; displayName: string }>, + options: { + all?: boolean; + category?: string; + tags?: string; + name?: string; + }, +) { + const registry = await getRegistry(opts, { cache: true }); + + let selectedSkills = discoveredSkills; + + // Interactive selection when not --all and terminal supports interaction + if (!options.all && isInteractive()) { + const items = discoveredSkills.map((s) => ({ + value: s.slug, + label: s.displayName, + hint: s.folder.split("/").pop() ?? "", + })); + const selected = await searchMultiselect({ + message: `Select skills to publish (${discoveredSkills.length} found):`, + items, + required: true, + }); + + if (typeof selected === "symbol") { + console.log("Upload cancelled"); + return; + } + + const selectedSlugs = selected as string[]; + selectedSkills = discoveredSkills.filter((s) => selectedSlugs.includes(s.slug)); + + if (selectedSkills.length === 0) { + console.log("No skills selected"); + return; + } + } + + // Derive package name from folder basename (e.g. "demo-multi-skill-folders") + const packageBaseName = options.name?.trim() || basename(folder); + const zipFileName = `${packageBaseName}.zip`; + + // Pack selected skills into a ZIP + const spinner = createSpinner( + `Packing ${selectedSkills.length} skill(s) into ZIP`, + ); + const zip = new AdmZip(); + + for (const skill of selectedSkills) { + zip.addLocalFolder(skill.folder, skill.slug); + } + + const zipBuffer = zip.toBuffer(); + spinner.text = `Uploading ${selectedSkills.length} skill(s) as ${zipFileName}`; + + // Upload via /api/skills/import-file + try { + const form = new FormData(); + const blob = new Blob([zipBuffer], { type: "application/zip" }); + form.set("file", blob, zipFileName); + if (packageBaseName) form.set("packageName", packageBaseName); + if (options.category) form.set("category", options.category); + if (options.tags) form.set("tags", options.tags); + + const result = await apiRequestForm<{ + success: boolean; + data: { + importedCount: number; + refreshedCount: number; + importedSkills: Array<{ slug: string; name: string }>; + }; + }>(registry, { + method: "POST", + path: "/api/skills/import-file", + form, + }); + + const data = result.data; + spinner.succeed( + `✓ Uploaded ${data.importedCount} skill(s)` + + (data.importedCount > 1 ? ` (package created)` : ""), + ); + + for (const skill of data.importedSkills) { + console.log(` - ${skill.name} (${skill.slug})`); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +/** + * Resolve folder argument: try workdir-relative first, then cwd-relative. + * This ensures `dt-skill publish ./my-folder` works regardless of whether + * workdir points to a clawdbot workspace or cwd. + */ +async function resolveFolderPath(workdir: string, folderArg: string): Promise { + const fromWorkdir = resolve(workdir, folderArg); + const workdirStat = await stat(fromWorkdir).catch(() => null); + if (workdirStat?.isDirectory()) return fromWorkdir; + + const fromCwd = resolve(process.cwd(), folderArg); + const cwdStat = await stat(fromCwd).catch(() => null); + if (cwdStat?.isDirectory()) return fromCwd; + + // Return the workdir-relative path so the original "Path must be a folder" error fires + return fromWorkdir; +} diff --git a/dt-skill/src/cli/commands/publishers.test.ts b/dt-skill/src/cli/commands/publishers.test.ts new file mode 100644 index 00000000..075f6fad --- /dev/null +++ b/dt-skill/src/cli/commands/publishers.test.ts @@ -0,0 +1,68 @@ +/* @vitest-environment node */ + +import { describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); + +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); + +const { cmdCreatePublisher } = await import("./publishers"); + +const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); +const mockWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + +function makeOpts(workdir = "/work") { + return makeGlobalOpts(workdir); +} + +describe("publisher CLI commands", () => { + it("creates an org publisher through the v1 publishers API", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + publisherId: "publishers:opik", + handle: "opik", + created: true, + trusted: false, + }); + + await cmdCreatePublisher(makeOpts(), "Opik", { displayName: "Opik" }); + + expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ + method: "POST", + path: "/api/v1/publishers", + token: "tkn", + body: { handle: "opik", displayName: "Opik" }, + }), + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("OK. Created publisher @opik."); + }); + + it("prints JSON for created org publishers", async () => { + const response = { + ok: true, + publisherId: "publishers:opik", + handle: "opik", + created: true, + trusted: false, + }; + httpMocks.apiRequest.mockResolvedValueOnce(response); + + await cmdCreatePublisher(makeOpts(), "opik", { json: true }); + + expect(mockWrite).toHaveBeenCalledWith(`${JSON.stringify(response, null, 2)}\n`); + }); +}); diff --git a/dt-skill/src/cli/commands/publishers.ts b/dt-skill/src/cli/commands/publishers.ts new file mode 100644 index 00000000..df833926 --- /dev/null +++ b/dt-skill/src/cli/commands/publishers.ts @@ -0,0 +1,47 @@ +import { apiRequest } from "../../http.js"; +import { ApiRoutes, ApiV1PublisherCreateResponseSchema } from "../../schema/index.js"; +import { requireAuthToken } from "../authToken.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; + +type PublisherCreateOptions = { + displayName?: string; + json?: boolean; +}; + +function normalizePublisherHandleOrFail(handle: string) { + const normalized = handle.trim().replace(/^@+/, "").toLowerCase(); + if (!normalized) throw new Error("Publisher handle is required"); + return normalized; +} + +export async function cmdCreatePublisher( + opts: GlobalOpts, + handle: string, + options: PublisherCreateOptions = {}, +) { + const normalizedHandle = normalizePublisherHandleOrFail(handle); + const displayName = options.displayName?.trim(); + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const result = await apiRequest( + registry, + { + method: "POST", + path: ApiRoutes.publishers, + token, + body: { + handle: normalizedHandle, + ...(displayName ? { displayName } : {}), + }, + }, + ApiV1PublisherCreateResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + + console.log(`OK. Created publisher @${result.handle}.`); +} diff --git a/dt-skill/src/cli/commands/skills.install.test.ts b/dt-skill/src/cli/commands/skills.install.test.ts new file mode 100644 index 00000000..ffb9998c --- /dev/null +++ b/dt-skill/src/cli/commands/skills.install.test.ts @@ -0,0 +1,362 @@ +/* @vitest-environment node */ + +import * as fsPromises from "node:fs/promises"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; +import * as skillStore from "../../skills.js"; + +const fsMocks = vi.hoisted(() => ({ + mkdir: vi.fn(), + rm: vi.fn(), + stat: vi.fn(), +})); + +vi.mock("node:fs/promises", async () => { + const actual = await vi.importActual("node:fs/promises"); + return { + ...actual, + mkdir: fsMocks.mkdir, + rm: fsMocks.rm, + stat: fsMocks.stat, + }; +}); + +const mocked = (value: T) => value as T & Record; +Object.assign(vi as object, { mocked }); + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); +const mockApiRequest = httpMocks.apiRequest; +const mockDownloadZip = httpMocks.downloadZip; +const mockGetOptionalAuthToken = authTokenMocks.getOptionalAuthToken; +const mockSpinner = uiMocks.spinner; +const mockIsInteractive = vi.fn(() => false); +const mockPromptConfirm = vi.fn(async () => false); +const mockSelectAgent = vi.fn(async () => null); +const mockSelectScope = vi.fn(async () => false); + +const mockSearchMultiselect = vi.fn(); + +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../ui.js", () => ({ + createSpinner: vi.fn(() => mockSpinner), + fail: (message: string) => uiMocks.fail(message), + formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), + isInteractive: mockIsInteractive, + promptConfirm: mockPromptConfirm, + selectAgent: mockSelectAgent, + selectScope: mockSelectScope, +})); + +vi.mock("../prompts/search-multiselect.js", async () => { + const actual = await vi.importActual("../prompts/search-multiselect.js"); + return { + ...actual, + searchMultiselect: (opts: any) => mockSearchMultiselect(opts), + }; +}); + +const extractZipToDirMock = vi.spyOn(skillStore, "extractZipToDir"); +const hashSkillFilesMock = vi.spyOn(skillStore, "hashSkillFiles"); +const listTextFilesMock = vi.spyOn(skillStore, "listTextFiles"); +const readLockfileMock = vi.spyOn(skillStore, "readLockfile"); +const readSkillOriginMock = vi.spyOn(skillStore, "readSkillOrigin"); +const writeLockfileMock = vi.spyOn(skillStore, "writeLockfile"); +const writeSkillOriginMock = vi.spyOn(skillStore, "writeSkillOrigin"); + +const mkdirMock = fsMocks.mkdir; +const rmMock = fsMocks.rm; +const statMock = fsMocks.stat; +const { cmdInstall } = await import("./skills.js"); + +const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); + +function makeOpts() { + return makeGlobalOpts(); +} + +beforeEach(() => { + process.exitCode = undefined; + mkdirMock.mockResolvedValue(undefined); + rmMock.mockResolvedValue(undefined); + statMock.mockRejectedValue(new Error("missing")); + extractZipToDirMock.mockResolvedValue(undefined); + hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: [] }); + listTextFilesMock.mockResolvedValue([]); + readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); + readSkillOriginMock.mockResolvedValue(null); + writeLockfileMock.mockResolvedValue(undefined); + writeSkillOriginMock.mockResolvedValue(undefined); + mockGetOptionalAuthToken.mockResolvedValue(undefined); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +afterAll(() => { + extractZipToDirMock.mockRestore(); + hashSkillFilesMock.mockRestore(); + listTextFilesMock.mockRestore(); + readLockfileMock.mockRestore(); + readSkillOriginMock.mockRestore(); + writeLockfileMock.mockRestore(); + writeSkillOriginMock.mockRestore(); +}); + +describe("cmdInstall with packages", () => { + it("installs single non-package skill directly", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockResolvedValue({ + skill: { + slug: "single-skill", + displayName: "Single Skill", + isPackage: 0, + }, + latestVersion: { version: "1.0.0" }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), "single-skill"); + + expect(mockApiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ path: "/api/v1/skills/single-skill" }), + expect.anything() + ); + expect(mockDownloadZip).toHaveBeenCalledWith( + "https://clawhub.ai", + { slug: "single-skill", version: "1.0.0" } + ); + expect(extractZipToDirMock).toHaveBeenCalled(); + expect(writeLockfileMock).toHaveBeenCalled(); + }); + + it("installs multiple skills in batch", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockImplementation(async (_registry, request) => { + const slug = (request as any).path?.split("/").pop(); + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: "1.0.0" }, + }; + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); + + expect(mockApiRequest).toHaveBeenCalledTimes(3); + expect(mockDownloadZip).toHaveBeenCalledTimes(3); + expect(extractZipToDirMock).toHaveBeenCalledTimes(3); + expect(writeLockfileMock).toHaveBeenCalledTimes(3); + }); + + it("installs skill when latestVersion is null", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockResolvedValue({ + skill: { + slug: "no-version-skill", + displayName: "No Version Skill", + isPackage: 0, + }, + latestVersion: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), "no-version-skill"); + + expect(mockApiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ path: "/api/v1/skills/no-version-skill" }), + expect.anything() + ); + expect(mockDownloadZip).toHaveBeenCalledWith( + "https://clawhub.ai", + { slug: "no-version-skill", version: "latest" } + ); + expect(extractZipToDirMock).toHaveBeenCalled(); + expect(writeLockfileMock).toHaveBeenCalled(); + }); + + it("continues batch install when one skill fails", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + let callCount = 0; + mockApiRequest.mockImplementation(async (_registry, request) => { + callCount++; + const slug = (request as any).path?.split("/").pop(); + if (slug === "skill-b") { + throw new Error("not found"); + } + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: "1.0.0" }, + }; + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); + + expect(mockApiRequest).toHaveBeenCalledTimes(3); + expect(mockDownloadZip).toHaveBeenCalledTimes(2); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Summary")); + expect(process.exitCode).toBe(1); + }); + + it("continues batch install when fail() is triggered for one skill", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockImplementation(async (_registry, request) => { + const slug = (request as any).path?.split("/").pop(); + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: "1.0.0" }, + }; + }); + statMock.mockImplementation(async (path: string) => { + if (path.includes("skill-b")) { + return { isFile: () => true, isDirectory: () => false } as any; + } + throw new Error("missing"); + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); + + expect(mockApiRequest).toHaveBeenCalledTimes(3); + expect(mockDownloadZip).toHaveBeenCalledTimes(2); + expect(mockSpinner.fail).toHaveBeenCalledWith(expect.stringContaining("Already installed")); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Summary")); + }); + + it("fails if package has no children skills", async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: "empty-package", + displayName: "Empty Package", + isPackage: 1, + children: [], + }, + latestVersion: { version: "1.0.0" }, + }); + + await expect(cmdInstall(makeOpts(), "empty-package")).rejects.toThrow( + 'Skill package "empty-package" has no children skills.' + ); + }); + + it("installs selected sub-skills from package using searchMultiselect", async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: "my-package", + displayName: "My Package", + isPackage: 1, + children: [ + { slug: "sub-1", displayName: "Sub 1", version: "1.1.0", summary: "Hint 1" }, + { slug: "sub-2", displayName: "Sub 2", version: "1.2.0", summary: "Hint 2" }, + ], + }, + latestVersion: { version: "1.0.0" }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + mockSearchMultiselect.mockResolvedValue(["sub-2"]); + + await cmdInstall(makeOpts(), "my-package"); + + expect(mockSearchMultiselect).toHaveBeenCalledWith({ + message: 'Select skills from package "my-package" to install:', + items: [ + { value: "sub-1", label: "Sub 1", hint: "Hint 1" }, + { value: "sub-2", label: "Sub 2", hint: "Hint 2" }, + ], + required: true, + }); + + expect(mockDownloadZip).toHaveBeenCalledTimes(1); + expect(mockDownloadZip).toHaveBeenCalledWith( + "https://clawhub.ai", + { slug: "sub-2", version: "1.2.0" } + ); + + expect(extractZipToDirMock).toHaveBeenCalledTimes(1); + expect(writeSkillOriginMock).toHaveBeenCalledTimes(1); + expect(writeLockfileMock).toHaveBeenCalledTimes(1); + }); + + it("installs all sub-skills when user selects all from package", async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: "full-package", + displayName: "Full Package", + isPackage: 1, + children: [ + { slug: "child-a", displayName: "Child A", version: "1.0.0" }, + { slug: "child-b", displayName: "Child B", version: "2.0.0" }, + { slug: "child-c", displayName: "Child C", version: "3.0.0" }, + ], + }, + latestVersion: { version: "1.0.0" }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + mockSearchMultiselect.mockResolvedValue(["child-a", "child-b", "child-c"]); + + await cmdInstall(makeOpts(), "full-package"); + + expect(mockDownloadZip).toHaveBeenCalledTimes(3); + expect(extractZipToDirMock).toHaveBeenCalledTimes(3); + expect(writeLockfileMock).toHaveBeenCalledTimes(3); + }); + + it("handles user cancellation in searchMultiselect", async () => { + const { cancelSymbol: realCancelSymbol } = await import("../prompts/search-multiselect.js"); + mockApiRequest.mockResolvedValue({ + skill: { + slug: "my-package", + displayName: "My Package", + isPackage: 1, + children: [ + { slug: "sub-1", displayName: "Sub 1" }, + ], + }, + latestVersion: { version: "1.0.0" }, + }); + mockSearchMultiselect.mockResolvedValue(realCancelSymbol); + + await cmdInstall(makeOpts(), "my-package"); + + expect(mockLog).toHaveBeenCalledWith("Installation cancelled"); + expect(mockDownloadZip).not.toHaveBeenCalled(); + }); + + it("prompts agent only once during batch install", async () => { + mockIsInteractive.mockReturnValue(true); + mockSelectAgent.mockResolvedValue({ + agent: "claude-code", + workdir: "/mock/.claude", + dir: "/mock/.claude/skills", + }); + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockImplementation(async (_registry, request) => { + const slug = (request as any).path?.split("/").pop(); + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: "1.0.0" }, + }; + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); + + expect(mockSelectAgent).toHaveBeenCalledTimes(1); + expect(mkdirMock).toHaveBeenCalledWith("/mock/.claude/skills", { recursive: true }); + expect(mockDownloadZip).toHaveBeenCalledTimes(3); + }); +}); diff --git a/dt-skill/src/cli/commands/skills.test.ts b/dt-skill/src/cli/commands/skills.test.ts new file mode 100644 index 00000000..fc302b5e --- /dev/null +++ b/dt-skill/src/cli/commands/skills.test.ts @@ -0,0 +1,926 @@ +/* @vitest-environment node */ + +import * as fsPromises from "node:fs/promises"; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; +import { ApiRoutes } from "../../schema/index.js"; +import * as skillStore from "../../skills.js"; + +const fsMocks = vi.hoisted(() => ({ + mkdir: vi.fn(), + rm: vi.fn(), + stat: vi.fn(), +})); + +vi.mock("node:fs/promises", async () => { + const actual = await vi.importActual("node:fs/promises"); + return { + ...actual, + mkdir: fsMocks.mkdir, + rm: fsMocks.rm, + stat: fsMocks.stat, + }; +}); + +const mocked = (value: T) => value as T & Record; +Object.assign(vi as object, { mocked }); + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); +const mockApiRequest = httpMocks.apiRequest; +const mockDownloadZip = httpMocks.downloadZip; +const mockGetOptionalAuthToken = authTokenMocks.getOptionalAuthToken; +const mockSpinner = uiMocks.spinner; +const mockIsInteractive = vi.fn(() => false); +const mockPromptConfirm = vi.fn(async () => false); +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +const mockSelectAgent = vi.fn(async () => null); +vi.mock("../ui.js", () => ({ + createSpinner: vi.fn(() => mockSpinner), + fail: (message: string) => uiMocks.fail(message), + formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), + isInteractive: mockIsInteractive, + promptConfirm: mockPromptConfirm, + selectAgent: mockSelectAgent, +})); + +const extractZipToDirMock = vi.spyOn(skillStore, "extractZipToDir"); +const hashSkillFilesMock = vi.spyOn(skillStore, "hashSkillFiles"); +const listTextFilesMock = vi.spyOn(skillStore, "listTextFiles"); +const readLockfileMock = vi.spyOn(skillStore, "readLockfile"); +const readSkillOriginMock = vi.spyOn(skillStore, "readSkillOrigin"); +const writeLockfileMock = vi.spyOn(skillStore, "writeLockfile"); +const writeSkillOriginMock = vi.spyOn(skillStore, "writeSkillOrigin"); + +const mkdirMock = fsMocks.mkdir; +const rmMock = fsMocks.rm; +const statMock = fsMocks.stat; +const { + clampLimit, + cmdExplore, + cmdInstall, + cmdList, + cmdListSkillReports, + cmdPin, + cmdReportSkill, + cmdSearch, + cmdTriageSkillReport, + cmdUninstall, + cmdUnpin, + cmdUpdate, + formatExploreLine, +} = await import("./skills.js"); +const { + extractZipToDir, + hashSkillFiles, + listTextFiles, + readLockfile, + readSkillOrigin, + writeLockfile, + writeSkillOrigin, +} = skillStore; +const { rm, stat } = fsPromises; + +const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); + +function makeOpts() { + return makeGlobalOpts(); +} + +beforeEach(() => { + mkdirMock.mockResolvedValue(undefined); + rmMock.mockResolvedValue(undefined); + statMock.mockRejectedValue(new Error("missing")); + extractZipToDirMock.mockResolvedValue(undefined); + hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: [] }); + listTextFilesMock.mockResolvedValue([]); + readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); + readSkillOriginMock.mockResolvedValue(null); + writeLockfileMock.mockResolvedValue(undefined); + writeSkillOriginMock.mockResolvedValue(undefined); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +afterAll(() => { + extractZipToDirMock.mockRestore(); + hashSkillFilesMock.mockRestore(); + listTextFilesMock.mockRestore(); + readLockfileMock.mockRestore(); + readSkillOriginMock.mockRestore(); + writeLockfileMock.mockRestore(); + writeSkillOriginMock.mockRestore(); +}); + +describe("explore helpers", () => { + it("clamps explore limits and handles non-finite values", () => { + expect(clampLimit(-5)).toBe(1); + expect(clampLimit(0)).toBe(1); + expect(clampLimit(1)).toBe(1); + expect(clampLimit(50)).toBe(50); + expect(clampLimit(99)).toBe(99); + expect(clampLimit(200)).toBe(200); + expect(clampLimit(250)).toBe(200); + expect(clampLimit(Number.NaN)).toBe(25); + expect(clampLimit(Number.POSITIVE_INFINITY)).toBe(25); + expect(clampLimit(Number.NaN, 10)).toBe(10); + }); + + it("formats explore lines with relative time and truncation", () => { + const now = 4 * 60 * 60 * 1000; + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(now); + const summary = "a".repeat(60); + const line = formatExploreLine({ + slug: "weather", + summary, + updatedAt: now - 2 * 60 * 60 * 1000, + latestVersion: null, + }); + expect(line).toBe(`weather v? 2h ago ${"a".repeat(49)}…`); + nowSpy.mockRestore(); + }); +}); + +describe("cmdExplore", () => { + it("does not attach a stored auth token to apiRequest", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockResolvedValue({ items: [] }); + + await cmdExplore(makeOpts(), { limit: 25 }); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + expect(mockGetOptionalAuthToken).not.toHaveBeenCalled(); + expect(requestArgs?.token).toBeUndefined(); + }); + + it("clamps limit and handles empty results", async () => { + mockApiRequest.mockResolvedValue({ items: [] }); + + await cmdExplore(makeOpts(), { limit: 0 }); + + const [, args] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(args?.url)); + expect(url.searchParams.get("limit")).toBe("1"); + expect(mockLog).toHaveBeenCalledWith("No skills found."); + }); + + it("prints formatted results", async () => { + const now = 10 * 60 * 1000; + const nowSpy = vi.spyOn(Date, "now").mockReturnValue(now); + const item = { + slug: "gog", + summary: "Google Workspace CLI for Gmail, Calendar, Drive and more.", + updatedAt: now - 90 * 1000, + latestVersion: { version: "1.2.3" }, + }; + mockApiRequest.mockResolvedValue({ items: [item] }); + + await cmdExplore(makeOpts(), { limit: 250 }); + + const [, args] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(args?.url)); + expect(url.searchParams.get("limit")).toBe("200"); + expect(mockLog).toHaveBeenCalledWith(formatExploreLine(item)); + nowSpy.mockRestore(); + }); + + it("supports sort and json output", async () => { + const payload = { items: [], nextCursor: null }; + mockApiRequest.mockResolvedValue(payload); + + await cmdExplore(makeOpts(), { limit: 10, sort: "installs", json: true }); + + const [, args] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(args?.url)); + expect(url.searchParams.get("limit")).toBe("10"); + expect(url.searchParams.get("sort")).toBe("installsCurrent"); + expect(mockLog).toHaveBeenCalledWith(JSON.stringify(payload, null, 2)); + }); + + it("supports all-time installs and trending sorts", async () => { + mockApiRequest.mockResolvedValue({ items: [], nextCursor: null }); + + await cmdExplore(makeOpts(), { limit: 5, sort: "newest" }); + await cmdExplore(makeOpts(), { limit: 5, sort: "installsAllTime" }); + await cmdExplore(makeOpts(), { limit: 5, sort: "trending" }); + + const first = new URL(String(mockApiRequest.mock.calls[0]?.[1]?.url)); + const second = new URL(String(mockApiRequest.mock.calls[1]?.[1]?.url)); + const third = new URL(String(mockApiRequest.mock.calls[2]?.[1]?.url)); + expect(first.searchParams.get("sort")).toBe("createdAt"); + expect(second.searchParams.get("sort")).toBe("installsAllTime"); + expect(third.searchParams.get("sort")).toBe("trending"); + }); +}); + +describe("cmdSearch", () => { + it("does not attach a stored auth token to apiRequest", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockResolvedValue({ results: [] }); + + await cmdSearch(makeOpts(), "demo"); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + expect(mockGetOptionalAuthToken).not.toHaveBeenCalled(); + expect(requestArgs?.token).toBeUndefined(); + }); + + it("defaults limit to 25 when not specified", async () => { + mockGetOptionalAuthToken.mockResolvedValue(undefined); + mockApiRequest.mockResolvedValue({ results: [] }); + + await cmdSearch(makeOpts(), "stock price"); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(requestArgs?.url)); + expect(url.searchParams.get("limit")).toBe("25"); + }); + + it("uses explicit limit when provided", async () => { + mockGetOptionalAuthToken.mockResolvedValue(undefined); + mockApiRequest.mockResolvedValue({ results: [] }); + + await cmdSearch(makeOpts(), "stock price", 5); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(requestArgs?.url)); + expect(url.searchParams.get("limit")).toBe("5"); + }); + + it("prints skill owners in search results", async () => { + mockGetOptionalAuthToken.mockResolvedValue(undefined); + mockApiRequest.mockResolvedValue({ + results: [ + { + slug: "demo", + displayName: "Demo Skill", + version: "1.2.3", + ownerHandle: "openclaw", + score: 0.9876, + }, + { + slug: "legacy", + displayName: "Legacy Skill", + version: null, + owner: { displayName: "Legacy Owner" }, + score: 0.5, + }, + ], + }); + + await cmdSearch(makeOpts(), "demo"); + + expect(mockLog).toHaveBeenCalledWith("demo v1.2.3 @openclaw Demo Skill (0.988)"); + expect(mockLog).toHaveBeenCalledWith("legacy Legacy Owner Legacy Skill (0.500)"); + }); +}); + +describe("skill moderation commands", () => { + it("submits skill reports", async () => { + mockApiRequest.mockResolvedValueOnce({ + ok: true, + reported: true, + alreadyReported: false, + reportId: "skillReports:1", + skillId: "skills:1", + reportCount: 1, + }); + + await cmdReportSkill(makeOpts(), "demo", { version: "1.0.0", reason: "suspicious files" }); + + expect(mockApiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/skills/demo/report", + token: "tkn", + body: { reason: "suspicious files", version: "1.0.0" }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith("OK. Reported demo (skillReports:1)."); + }); + + it("lists skill reports", async () => { + mockApiRequest.mockResolvedValueOnce({ + items: [ + { + reportId: "skillReports:1", + skillId: "skills:1", + skillVersionId: "skillVersions:1", + slug: "demo", + displayName: "Demo", + version: "1.0.0", + reason: "suspicious", + status: "open", + createdAt: 1, + reporter: { userId: "users:reporter", handle: "reporter", displayName: "Reporter" }, + triagedAt: null, + triagedBy: null, + triageNote: null, + }, + ], + nextCursor: null, + done: true, + }); + + await cmdListSkillReports(makeOpts(), { status: "open", limit: 10 }); + + const request = mockApiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; + const url = new URL(String(request?.url)); + expect(url.pathname).toBe("/api/v1/skills/-/reports"); + expect(url.searchParams.get("status")).toBe("open"); + expect(url.searchParams.get("limit")).toBe("10"); + expect(mockLog).toHaveBeenCalledWith("skillReports:1 open demo"); + }); + + it("triages skill reports", async () => { + mockApiRequest.mockResolvedValueOnce({ + ok: true, + reportId: "skillReports:1", + skillId: "skills:1", + status: "confirmed", + reportCount: 0, + actionTaken: "hide", + }); + + await cmdTriageSkillReport(makeOpts(), "skillReports:1", { + status: "confirmed", + note: "handled", + action: "hide", + yes: true, + }); + + expect(mockApiRequest).toHaveBeenCalledWith( + "https://clawhub.ai", + { + method: "POST", + path: "/api/v1/skills/-/reports/skillReports%3A1/triage", + token: "tkn", + body: { status: "confirmed", note: "handled", finalAction: "hide" }, + }, + expect.anything(), + ); + expect(mockLog).toHaveBeenCalledWith( + "OK. Skill report skillReports:1 set to confirmed; action hide.", + ); + expect(mockLog).toHaveBeenCalledWith(" - Hide the skill from public availability."); + }); +}); + +describe("cmdUpdate", () => { + it("fails when directly updating a pinned skill", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: "0.1.0", installedAt: 123, pinned: true, pinReason: "hold" }, + }, + }); + + await expect(cmdUpdate(makeOpts(), "demo", { force: true }, false)).rejects.toThrow( + /is pinned/i, + ); + + expect(mockApiRequest).not.toHaveBeenCalled(); + expect(mockDownloadZip).not.toHaveBeenCalled(); + }); + + it("skips pinned skills during update --all and reports them in the summary", async () => { + mockApiRequest.mockResolvedValue({ + latestVersion: { version: "2.0.0" }, + moderation: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: "0.1.0", installedAt: 123, pinned: true, pinReason: "hold" }, + other: { version: "1.0.0", installedAt: 456 }, + }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: "hash", files: [] }); + vi.mocked(stat).mockRejectedValue(new Error("missing")); + vi.mocked(rm).mockResolvedValue(); + + await cmdUpdate(makeOpts(), undefined, { all: true }, false); + + expect(mockApiRequest).toHaveBeenCalledTimes(1); + const [, args] = mockApiRequest.mock.calls[0] ?? []; + expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent("other")}`); + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: { + demo: { version: "0.1.0", installedAt: 123, pinned: true, pinReason: "hold" }, + other: { version: "2.0.0", installedAt: expect.any(Number) }, + }, + }); + expect(mockLog).toHaveBeenCalledWith("Skipped 1 pinned skill: demo"); + }); + + it("uses path-based skill lookup when no local fingerprint is available", async () => { + mockApiRequest.mockResolvedValue({ latestVersion: { version: "1.0.0" } }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "0.1.0", installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: "hash", files: [] }); + vi.mocked(stat).mockRejectedValue(new Error("missing")); + vi.mocked(rm).mockResolvedValue(); + + await cmdUpdate(makeOpts(), "demo", {}, false); + + const [, args] = mockApiRequest.mock.calls[0] ?? []; + expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent("demo")}`); + expect(args?.url).toBeUndefined(); + }); + + it("trusts the stored install fingerprint when the resolve endpoint cannot match", async () => { + mockApiRequest + .mockResolvedValueOnce({ + latestVersion: { version: "2.0.0" }, + moderation: null, + }) + .mockResolvedValueOnce({ + match: null, + latestVersion: { version: "2.0.0" }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue({ + version: 1, + registry: "https://clawhub.ai", + slug: "demo", + installedVersion: "1.0.0", + installedAt: 123, + fingerprint: "hash", + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(listTextFiles).mockResolvedValue([ + { relPath: "SKILL.md", bytes: new Uint8Array([1]) }, + ]); + vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: "hash", files: [] }); + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); + vi.mocked(rm).mockResolvedValue(); + + await cmdUpdate(makeOpts(), "demo", {}, false); + + expect(mockLog).not.toHaveBeenCalledWith( + "demo: local changes (no match). Use --force to overwrite.", + ); + expect(mockDownloadZip).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ slug: "demo", version: "2.0.0" }), + ); + expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/demo", { + version: 1, + registry: "https://clawhub.ai", + slug: "demo", + installedVersion: "2.0.0", + installedAt: 123, + fingerprint: "hash", + }); + }); +}); + +describe("pin commands", () => { + it("pins an installed skill and preserves its version metadata", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + + await cmdPin(makeOpts(), "demo", { reason: "scanner hold" }); + + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: { + demo: { + version: "1.0.0", + installedAt: 123, + pinned: true, + pinReason: "scanner hold", + }, + }, + }); + expect(mockLog).toHaveBeenCalledWith("Pinned demo: scanner hold"); + }); + + it("reports when an installed skill is already pinned without changes", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: "1.0.0", installedAt: 123, pinned: true, pinReason: "scanner hold" }, + }, + }); + + await cmdPin(makeOpts(), "demo"); + + expect(writeLockfile).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledWith('Skill "demo" is already pinned: scanner hold'); + }); + + it("unpinned skills clear pin metadata and keep the installed version", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: "1.0.0", installedAt: 123, pinned: true, pinReason: "scanner hold" }, + }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + + await cmdUnpin(makeOpts(), "demo"); + + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: { + demo: { + version: "1.0.0", + installedAt: 123, + }, + }, + }); + expect(mockLog).toHaveBeenCalledWith("Unpinned demo"); + }); +}); + +describe("cmdList", () => { + it("shows pinned state in list output", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: "1.0.0", installedAt: 123, pinned: true, pinReason: "scanner hold" }, + other: { version: "2.0.0", installedAt: 456 }, + }, + }); + + await cmdList(makeOpts()); + + expect(mockLog).toHaveBeenCalledWith("demo 1.0.0 pinned (scanner hold)"); + expect(mockLog).toHaveBeenCalledWith("other 2.0.0"); + }); +}); + +describe("cmdInstall", () => { + it("does not attach a stored auth token to API or download requests", async () => { + mockGetOptionalAuthToken.mockResolvedValue("tkn"); + mockApiRequest.mockResolvedValue({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: "1.0.0" }, + owner: null, + moderation: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(stat).mockRejectedValue(new Error("missing")); + vi.mocked(rm).mockResolvedValue(); + + await cmdInstall(makeOpts(), "demo"); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + expect(mockGetOptionalAuthToken).not.toHaveBeenCalled(); + expect(requestArgs?.token).toBeUndefined(); + const [, zipArgs] = mockDownloadZip.mock.calls[0] ?? []; + expect(zipArgs?.token).toBeUndefined(); + }); + + it("blocks force reinstall when a skill is pinned", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "0.9.0", installedAt: 123, pinned: true, pinReason: "hold" } }, + }); + vi.mocked(stat).mockRejectedValue(new Error("missing")); + + await expect(cmdInstall(makeOpts(), "demo", undefined, true)).rejects.toThrow(/is pinned/i); + + expect(mockApiRequest).not.toHaveBeenCalled(); + expect(mockDownloadZip).not.toHaveBeenCalled(); + expect(rm).not.toHaveBeenCalled(); + expect(writeLockfile).not.toHaveBeenCalled(); + }); + + it("does not rm local directory when skill is malware-blocked (--force)", async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest.mockResolvedValue({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: "1.0.0" }, + owner: null, + moderation: { isMalwareBlocked: true, isSuspicious: false }, + }); + + await expect(cmdInstall(makeOpts(), "demo", undefined, true)).rejects.toThrow(/malware/i); + + expect(rm).not.toHaveBeenCalled(); + }); + + it("does not rm local directory when API fetch fails (--force)", async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest.mockRejectedValue(new Error("Skill not found")); + + await expect(cmdInstall(makeOpts(), "demo", undefined, true)).rejects.toThrow(/not found/i); + + expect(rm).not.toHaveBeenCalled(); + }); + + it("does not rm local directory when requested version lookup fails (--force)", async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest + .mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: "1.0.0" }, + owner: null, + moderation: null, + }) + .mockRejectedValueOnce(new Error("Version not found")); + + await expect(cmdInstall(makeOpts(), "demo", "9.9.9", true)).rejects.toThrow( + /version not found/i, + ); + + expect(rm).not.toHaveBeenCalled(); + expect(mockApiRequest).toHaveBeenNthCalledWith( + 2, + "https://clawhub.ai", + expect.objectContaining({ + path: `${ApiRoutes.skills}/${encodeURIComponent("demo")}/versions/${encodeURIComponent("9.9.9")}`, + }), + expect.anything(), + ); + }); + + it("validates requested version before rm when all checks pass (--force)", async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest + .mockResolvedValueOnce({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: "1.0.0" }, + owner: null, + moderation: null, + }) + .mockResolvedValueOnce({ + version: { + version: "9.9.9", + createdAt: 0, + changelog: "", + changelogSource: null, + license: null, + files: [], + }, + skill: { slug: "demo", displayName: "Demo" }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdInstall(makeOpts(), "demo", "9.9.9", true); + + expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); + expect(mockDownloadZip).toHaveBeenCalledWith( + "https://clawhub.ai", + expect.objectContaining({ slug: "demo", version: "9.9.9" }), + ); + const versionLookupOrder = mockApiRequest.mock.invocationCallOrder[1]; + const rmOrder = vi.mocked(rm).mock.invocationCallOrder[0]; + const downloadOrder = mockDownloadZip.mock.invocationCallOrder[0]; + expect(versionLookupOrder).toBeLessThan(rmOrder); + expect(rmOrder).toBeLessThan(downloadOrder); + }); + + it("calls rm before download when all checks pass (--force)", async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest.mockResolvedValue({ + skill: { + slug: "demo", + displayName: "Demo", + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: "1.0.0" }, + owner: null, + moderation: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdInstall(makeOpts(), "demo", undefined, true); + + expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); + expect(mockDownloadZip).toHaveBeenCalled(); + const rmOrder = vi.mocked(rm).mock.invocationCallOrder[0]; + const downloadOrder = mockDownloadZip.mock.invocationCallOrder[0]; + expect(rmOrder).toBeLessThan(downloadOrder); + }); +}); + +describe("cmdUninstall", () => { + it("requires --yes when input is disabled", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + + await expect(cmdUninstall(makeOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); + }); + + it("prompts when interactive and proceeds on confirm", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + mockIsInteractive.mockReturnValue(true); + mockPromptConfirm.mockResolvedValue(true); + + await cmdUninstall(makeOpts(), "demo", {}, true); + + expect(mockPromptConfirm).toHaveBeenCalledWith("Uninstall demo?"); + expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); + expect(writeLockfile).toHaveBeenCalled(); + }); + + it("prints Cancelled and does not remove when prompt declines", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + mockIsInteractive.mockReturnValue(true); + mockPromptConfirm.mockResolvedValue(false); + + await cmdUninstall(makeOpts(), "demo", {}, true); + + expect(mockLog).toHaveBeenCalledWith("Cancelled."); + expect(rm).not.toHaveBeenCalled(); + expect(writeLockfile).not.toHaveBeenCalled(); + }); + + it("rejects unsafe slugs", async () => { + await expect(cmdUninstall(makeOpts(), "../evil", { yes: true }, false)).rejects.toThrow( + /invalid slug/i, + ); + await expect(cmdUninstall(makeOpts(), "demo/evil", { yes: true }, false)).rejects.toThrow( + /invalid slug/i, + ); + }); + + it("fails when skill is not installed", async () => { + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + + await expect(cmdUninstall(makeOpts(), "missing", {}, false)).rejects.toThrow( + "Not installed: missing", + ); + }); + + it("removes skill directory and lockfile entry with --yes flag", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdUninstall(makeOpts(), "demo", { yes: true }, false); + + expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: {}, + }); + expect(mockSpinner.succeed).toHaveBeenCalledWith("Uninstalled demo"); + }); + + it("does not update lockfile if remove fails", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(rm).mockRejectedValue(new Error("nope")); + + await expect(cmdUninstall(makeOpts(), "demo", { yes: true }, false)).rejects.toThrow("nope"); + + expect(writeLockfile).not.toHaveBeenCalled(); + }); + + it("updates lockfile after removing directory", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdUninstall(makeOpts(), "demo", { yes: true }, false); + + const rmCallMock = vi.mocked(rm); + const writeLockfileCallMock = vi.mocked(writeLockfile); + expect(rmCallMock.mock.invocationCallOrder[0]).toBeLessThan( + writeLockfileCallMock.mock.invocationCallOrder[0], + ); + }); + + it("removes skill and updates lockfile keeping other skills", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: "1.0.0", installedAt: 123 }, + other: { version: "2.0.0", installedAt: 456 }, + }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdUninstall(makeOpts(), "demo", { yes: true }, false); + + expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); + expect(writeLockfile).toHaveBeenCalledWith("/work", { + version: 1, + skills: { other: { version: "2.0.0", installedAt: 456 } }, + }); + }); + + it("trims slug whitespace", async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdUninstall(makeOpts(), " demo ", { yes: true }, false); + + expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); + }); +}); diff --git a/dt-skill/src/cli/commands/skills.ts b/dt-skill/src/cli/commands/skills.ts new file mode 100644 index 00000000..d0966529 --- /dev/null +++ b/dt-skill/src/cli/commands/skills.ts @@ -0,0 +1,1023 @@ +import { mkdir, rm, stat } from "node:fs/promises"; +import { join } from "node:path"; +import semver from "semver"; +import { apiRequest, downloadZip, registryUrl } from "../../http.js"; +import { + ApiRoutes, + ApiV1SearchResponseSchema, + ApiV1SkillListResponseSchema, + ApiV1SkillReportListResponseSchema, + ApiV1SkillReportResponseSchema, + ApiV1SkillReportTriageResponseSchema, + ApiV1SkillResolveResponseSchema, + ApiV1SkillResponseSchema, + ApiV1SkillVersionResponseSchema, + type ApiV1SearchResponse, + type ApiV1SkillListResponse, + type ApiV1SkillResponse, + type ApiV1SkillResolveResponse, + type ApiV1SkillReportResponse, + type ApiV1SkillReportListResponse, + type ApiV1SkillReportTriageResponse, + type SkillReportFinalAction, + type SkillReportListStatus, + type SkillReportStatus, +} from "../../schema/index.js"; +import { + extractZipToDir, + hashSkillFiles, + listManualSkills, + listTextFiles, + readLockfile, + readSkillOrigin, + writeLockfile, + writeSkillOrigin, +} from "../../skills.js"; +import { requireAuthToken } from "../authToken.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts, ResolveResult } from "../types.js"; +import { + createSpinner, + fail, + formatError, + isInteractive, + promptConfirm, + selectAgent, + selectScope, +} from "../ui.js"; +import { presentModerationPlan, reportModerationPlan } from "./moderationPlan.js"; +import { searchMultiselect, cancelSymbol } from "../prompts/search-multiselect.js"; +import { getAgentLabel, resolveAgentWorkdir } from "../agents.js"; +import type { AgentName } from "../agents.js"; + +type SkillReportOptions = { + version?: string; + reason?: string; + json?: boolean; +}; + +type SkillReportListOptions = { + status?: SkillReportListStatus; + cursor?: string; + limit?: number; + json?: boolean; +}; + +type SkillReportTriageOptions = { + status?: SkillReportStatus; + action?: SkillReportFinalAction; + finalAction?: SkillReportFinalAction; + note?: string; + json?: boolean; + yes?: boolean; +}; + +function normalizeSkillSlugOrFail(raw: string) { + const slug = raw.trim(); + if (!slug) fail("Slug required"); + // Safety: never allow path traversal or nested paths to become filesystem operations. + if (slug.includes("/") || slug.includes("\\") || slug.includes("..")) { + fail(`Invalid slug: ${slug}`); + } + return slug; +} + +function isSafeSkillSlug(slug: string) { + return Boolean(slug) && !slug.includes("/") && !slug.includes("\\") && !slug.includes(".."); +} + +function isPinnedSkillEntry(entry?: { pinned?: boolean | null }) { + return entry?.pinned === true; +} + +function withPinnedMetadata( + version: string | null, + installedAt: number, + existing?: { pinned?: boolean; pinReason?: string }, +) { + return { + version, + installedAt, + ...(existing?.pinned ? { pinned: true } : {}), + ...(existing?.pinned && existing.pinReason ? { pinReason: existing.pinReason } : {}), + }; +} + +function formatPinnedDetails(entry?: { pinReason?: string }) { + return entry?.pinReason ? ` (${entry.pinReason})` : ""; +} + +function formatSearchOwner(entry: { + ownerHandle?: string | null; + owner?: { handle?: string | null; displayName?: string | null } | null; +}) { + const handle = entry.ownerHandle ?? entry.owner?.handle; + if (handle) return `@${handle}`; + return entry.owner?.displayName ?? "unknown owner"; +} + +export async function cmdSearch(opts: GlobalOpts, query: string, limit?: number) { + if (!query) fail("Query required"); + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Searching"); + try { + const url = registryUrl(ApiRoutes.search, registry); + url.searchParams.set("q", query); + const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? limit : 25; + url.searchParams.set("limit", String(effectiveLimit)); + const result = await apiRequest( + registry, + { method: "GET", url: url.toString() }, + ApiV1SearchResponseSchema, + ); + + spinner.stop(); + for (const entry of result.results) { + const slug = entry.slug ?? "unknown"; + const name = entry.displayName ?? slug; + const version = entry.version ? ` v${entry.version}` : ""; + console.log( + `${slug}${version} ${formatSearchOwner(entry)} ${name} (${entry.score.toFixed(3)})`, + ); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdInstall( + opts: GlobalOpts, + rawSlug: string | string[], + versionFlag?: string, + force = false, +) { + if (Array.isArray(rawSlug)) { + let batchOpts = opts; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + batchOpts = { ...opts, agent: picked.agent, workdir: picked.workdir, dir: picked.dir }; + } + } + + // Scope selection for batch install (copied from vercel-labs/skills) + if (batchOpts.agent && !batchOpts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(batchOpts.agent as AgentName); + if (scope === null) { + console.log("Installation cancelled"); + return; + } + if (scope) { + const workdir = resolveAgentWorkdir(batchOpts.agent as AgentName, true); + batchOpts = { ...batchOpts, workdir, dir: `${workdir}/skills`, globalScope: true, globalScopeExplicit: true }; + } else { + batchOpts = { ...batchOpts, globalScope: false, globalScopeExplicit: true }; + } + } + + const results: { slug: string; status: 'ok' | 'fail' }[] = []; + for (const slug of rawSlug) { + try { + await cmdInstall(batchOpts, slug, versionFlag, force); + results.push({ slug, status: 'ok' }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.log(`✖ ${slug}: ${message}`); + results.push({ slug, status: 'fail' }); + } + } + const okCount = results.filter((r) => r.status === 'ok').length; + const failCount = results.filter((r) => r.status === 'fail').length; + console.log(`Summary: ${okCount} ok, ${failCount} fail`); + if (failCount > 0) { + process.exitCode = 1; + } + return; + } + + const trimmed = normalizeSkillSlugOrFail(rawSlug); + + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } + } + + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log("Installation cancelled"); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } + + const registry = await getRegistry(opts, { cache: true }); + await mkdir(installDir, { recursive: true }); + const target = join(installDir, trimmed); + + const lock = await readLockfile(installWorkdir); + const existingEntry = lock.skills[trimmed]; + if (isPinnedSkillEntry(existingEntry)) { + fail(`skill "${trimmed}" is pinned; run \`clawhub unpin ${trimmed}\` first`); + } + + const spinner = createSpinner(`Resolving ${trimmed}`); + try { + // Fetch skill metadata including moderation status + const skillMeta = await apiRequest( + registry, + { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}` }, + ApiV1SkillResponseSchema, + ); + + // Check moderation status before proceeding + if (skillMeta.moderation?.isMalwareBlocked) { + spinner.fail(`Blocked: ${trimmed} is flagged as malicious`); + fail("This skill has been flagged as malware and cannot be installed."); + } + + if (skillMeta.skill && (skillMeta.skill as any).isPackage) { + spinner.stop(); + const children = (skillMeta.skill as any).children || []; + if (children.length === 0) { + fail(`Skill package "${trimmed}" has no children skills.`); + } + + // Prepare items for searchMultiselect + const items = children.map((c: any) => ({ + value: c.slug, + label: c.displayName || c.slug, + hint: c.summary || undefined, + })); + + const selectedSlugs = await searchMultiselect({ + message: `Select skills from package "${trimmed}" to install:`, + items, + required: true, + }); + + if (selectedSlugs === cancelSymbol || !Array.isArray(selectedSlugs) || selectedSlugs.length === 0) { + console.log("Installation cancelled"); + return; + } + + // Install each selected sub-skill + for (const subSlug of selectedSlugs as string[]) { + const subSkill = children.find((c: any) => c.slug === subSlug); + const subVersion = String(subSkill?.version || "") || "latest"; + const subTarget = join(installDir, subSlug); + + if (!force) { + const exists = await fileExists(subTarget); + if (exists) { + console.log(`Already installed: ${subTarget} (skipping, use --force to overwrite)`); + continue; + } + } else { + await rm(subTarget, { recursive: true, force: true }); + } + + const subSpinner = createSpinner(`Downloading sub-skill ${subSlug}@${subVersion}`); + try { + const zip = await downloadZip(registry, { slug: subSlug, version: subVersion }); + await extractZipToDir(zip, subTarget); + const installedFiles = await listTextFiles(subTarget); + const installedFingerprint = + installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; + + await writeSkillOrigin(subTarget, { + version: 1, + registry, + slug: subSlug, + installedVersion: subVersion, + installedAt: Date.now(), + fingerprint: installedFingerprint, + }); + + lock.skills[subSlug] = withPinnedMetadata(subVersion, Date.now(), lock.skills[subSlug]); + await writeLockfile(installWorkdir, lock); + const agentSuffix2 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; + subSpinner.succeed(`OK. Installed sub-skill ${subSlug} -> ${subTarget}${agentSuffix2}`); + } catch (err) { + subSpinner.fail(`Failed to install sub-skill ${subSlug}: ${formatError(err)}`); + throw err; + } + } + return; + } + + if (!force) { + const exists = await fileExists(target); + if (exists) fail(`Already installed: ${target} (use --force)`); + } + + if (skillMeta.moderation?.isSuspicious && !force) { + spinner.stop(); + console.log( + `\n⚠️ Warning: "${trimmed}" is flagged for ClawHub security review.\n` + + " This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n" + + " Review the skill code before use.\n", + ); + if (isInteractive()) { + const confirm = await promptConfirm("Install anyway?"); + if (!confirm) fail("Installation cancelled"); + spinner.start(`Resolving ${trimmed}`); + } else { + fail("Use --force to install suspicious skills in non-interactive mode"); + } + } + + const resolvedVersion = versionFlag ?? skillMeta.latestVersion?.version ?? "latest"; + + if (versionFlag) { + await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions/${encodeURIComponent( + resolvedVersion, + )}`, + }, + ApiV1SkillVersionResponseSchema, + ); + } + + if (force) { + await rm(target, { recursive: true, force: true }); + } + + spinner.text = `Downloading ${trimmed}@${resolvedVersion}`; + const zip = await downloadZip(registry, { slug: trimmed, version: resolvedVersion }); + await extractZipToDir(zip, target); + const installedFiles = await listTextFiles(target); + const installedFingerprint = + installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; + + await writeSkillOrigin(target, { + version: 1, + registry, + slug: trimmed, + installedVersion: resolvedVersion, + installedAt: Date.now(), + fingerprint: installedFingerprint, + }); + + lock.skills[trimmed] = withPinnedMetadata(resolvedVersion, Date.now(), existingEntry); + await writeLockfile(installWorkdir, lock); + const agentSuffix = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; + spinner.succeed(`OK. Installed ${trimmed} -> ${target}${agentSuffix}`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdUpdate( + opts: GlobalOpts, + slugArg: string | undefined, + options: { all?: boolean; version?: string; force?: boolean }, + inputAllowed: boolean, +) { + const slug = slugArg ? normalizeSkillSlugOrFail(slugArg) : undefined; + const all = Boolean(options.all); + if (!slug && !all) fail("Provide or --all"); + if (slug && all) fail("Use either or --all"); + if (options.version && !slug) fail("--version requires a single "); + if (options.version && !semver.valid(options.version)) fail("--version must be valid semver"); + + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } + } + + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log("Update cancelled"); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } + + const lock = await readLockfile(installWorkdir); + if (slug && isPinnedSkillEntry(lock.skills[slug])) { + fail(`skill "${slug}" is pinned; run \`clawhub unpin ${slug}\` first`); + } + const allowPrompt = isInteractive() && inputAllowed; + + const registry = await getRegistry(opts, { cache: true }); + const requestedSlugs = slug ? [slug] : Object.keys(lock.skills).filter(isSafeSkillSlug); + const skippedPinned = slug + ? [] + : requestedSlugs.filter((entry) => isPinnedSkillEntry(lock.skills[entry])); + const slugs = slug + ? requestedSlugs + : requestedSlugs.filter((entry) => !isPinnedSkillEntry(lock.skills[entry])); + if (slugs.length === 0) { + if (skippedPinned.length > 0) { + const suffix = skippedPinned.length === 1 ? "" : "s"; + console.log( + `Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(", ")}`, + ); + return; + } + console.log("No installed skills."); + return; + } + + for (const entry of slugs) { + const spinner = createSpinner(`Checking ${entry}`); + try { + const target = join(installDir, entry); + const exists = await fileExists(target); + const existingOrigin = exists ? await readSkillOrigin(target) : null; + + // Always fetch skill metadata to check moderation status + const skillMeta = await apiRequest( + registry, + { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(entry)}` }, + ApiV1SkillResponseSchema, + ); + + // Check moderation status before proceeding + if (skillMeta.moderation?.isMalwareBlocked) { + spinner.fail(`${entry}: blocked as malicious`); + console.log(" This skill has been flagged as malware and cannot be updated."); + continue; + } + + if (skillMeta.moderation?.isSuspicious && !options.force) { + spinner.stop(); + console.log( + `\n⚠️ Warning: "${entry}" is flagged for ClawHub security review.\n` + + " This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n", + ); + if (allowPrompt) { + const confirm = await promptConfirm("Update anyway?"); + if (!confirm) { + console.log(`${entry}: skipped`); + continue; + } + spinner.start(`Checking ${entry}`); + } else { + console.log(`${entry}: skipped (use --force to update suspicious skills)`); + continue; + } + } + + let localFingerprint: string | null = null; + if (exists) { + const filesOnDisk = await listTextFiles(target); + if (filesOnDisk.length > 0) { + const hashed = hashSkillFiles(filesOnDisk); + localFingerprint = hashed.fingerprint; + } + } + + let resolveResult: ResolveResult; + if (localFingerprint) { + resolveResult = await resolveSkillVersion(registry, entry, localFingerprint); + } else { + resolveResult = { match: null, latestVersion: skillMeta.latestVersion ?? null }; + } + + const latest = resolveResult.latestVersion?.version ?? null; + const matched = + resolveResult.match?.version ?? + (localFingerprint && + existingOrigin?.fingerprint === localFingerprint && + existingOrigin.slug === entry + ? existingOrigin.installedVersion + : null); + + if (matched && lock.skills[entry]?.version !== matched) { + lock.skills[entry] = withPinnedMetadata( + matched, + lock.skills[entry]?.installedAt ?? Date.now(), + lock.skills[entry], + ); + } + + if (!latest) { + spinner.fail(`${entry}: not found`); + continue; + } + + if (!matched && localFingerprint && !options.force) { + spinner.stop(); + if (!allowPrompt) { + console.log(`${entry}: local changes (no match). Use --force to overwrite.`); + continue; + } + const confirm = await promptConfirm( + `${entry}: local changes (no match). Overwrite with ${options.version ?? latest}?`, + ); + if (!confirm) { + console.log(`${entry}: skipped`); + continue; + } + spinner.start(`Updating ${entry} -> ${options.version ?? latest}`); + } + + const targetVersion = options.version ?? latest; + if (options.version) { + if (matched && matched === targetVersion) { + spinner.succeed(`${entry}: already at ${matched}`); + continue; + } + } else if (matched && semver.valid(matched) && semver.gte(matched, targetVersion)) { + spinner.succeed(`${entry}: up to date (${matched})`); + continue; + } + + if (spinner.isSpinning) { + spinner.text = `Updating ${entry} -> ${targetVersion}`; + } else { + spinner.start(`Updating ${entry} -> ${targetVersion}`); + } + await rm(target, { recursive: true, force: true }); + const zip = await downloadZip(registry, { slug: entry, version: targetVersion }); + await extractZipToDir(zip, target); + const installedFiles = await listTextFiles(target); + const installedFingerprint = + installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; + + await writeSkillOrigin(target, { + version: 1, + registry: existingOrigin?.registry ?? registry, + slug: existingOrigin?.slug ?? entry, + installedVersion: targetVersion, + installedAt: existingOrigin?.installedAt ?? Date.now(), + fingerprint: installedFingerprint, + }); + + lock.skills[entry] = withPinnedMetadata(targetVersion, Date.now(), lock.skills[entry]); + const agentSuffix3 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; + spinner.succeed(`${entry}: updated -> ${targetVersion}${agentSuffix3}`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } + } + + await writeLockfile(installWorkdir, lock); + if (skippedPinned.length > 0) { + const suffix = skippedPinned.length === 1 ? "" : "s"; + console.log( + `Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(", ")}`, + ); + } +} + +export async function cmdList(opts: GlobalOpts) { + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } + } + + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log("List cancelled"); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } + + const lock = await readLockfile(installWorkdir); + const entries = Object.entries(lock.skills); + const manualSkills = await listManualSkills(installDir, new Set(Object.keys(lock.skills))); + if (installAgent) { + console.log(`Skills for ${getAgentLabel(installAgent as import("../agents.js").AgentName)} (${installDir}):`); + } + if (entries.length === 0 && manualSkills.length === 0) { + console.log("No installed skills."); + return; + } + for (const [slug, entry] of entries) { + const e = entry as { version?: string | null; pinned?: boolean; pinReason?: string }; + const pinned = isPinnedSkillEntry(e) ? ` pinned${formatPinnedDetails(e)}` : ""; + console.log(`${slug} ${e.version ?? "latest"}${pinned}`); + } + if (manualSkills.length > 0) { + if (entries.length > 0) console.log(); + console.log("Manually installed (not tracked by clawhub):"); + for (const slug of manualSkills) { + console.log(` ${slug}`); + } + } +} + +export async function cmdPin(opts: GlobalOpts, slug: string, options: { reason?: string } = {}) { + const trimmed = normalizeSkillSlugOrFail(slug); + const lock = await readLockfile(opts.workdir); + const existing = lock.skills[trimmed]; + if (!existing) fail(`Not installed: ${trimmed}`); + + const reason = options.reason?.trim() || existing.pinReason; + if (isPinnedSkillEntry(existing) && reason === existing.pinReason) { + console.log(`Skill "${trimmed}" is already pinned${reason ? `: ${reason}` : ""}`); + return; + } + + lock.skills[trimmed] = { + ...existing, + pinned: true, + ...(reason ? { pinReason: reason } : {}), + }; + await writeLockfile(opts.workdir, lock); + console.log(`Pinned ${trimmed}${reason ? `: ${reason}` : ""}`); +} + +export async function cmdUnpin(opts: GlobalOpts, slug: string) { + const trimmed = normalizeSkillSlugOrFail(slug); + const lock = await readLockfile(opts.workdir); + const existing = lock.skills[trimmed]; + if (!existing) fail(`Not installed: ${trimmed}`); + if (!isPinnedSkillEntry(existing)) fail(`Skill "${trimmed}" is not pinned`); + + lock.skills[trimmed] = { + version: existing.version, + installedAt: existing.installedAt, + }; + await writeLockfile(opts.workdir, lock); + console.log(`Unpinned ${trimmed}`); +} + +export async function cmdUninstall( + opts: GlobalOpts, + slug: string, + options: { yes?: boolean } = {}, + inputAllowed: boolean, +) { + const trimmed = normalizeSkillSlugOrFail(slug); + + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } + } + + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log("Uninstall cancelled"); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } + + const lock = await readLockfile(installWorkdir); + if (!lock.skills[trimmed]) { + fail(`Not installed: ${trimmed}`); + } + + const allowPrompt = isInteractive() && inputAllowed; + if (!options.yes) { + if (!allowPrompt) fail("Pass --yes (no input)"); + const confirm = await promptConfirm(`Uninstall ${trimmed}?`); + if (!confirm) { + console.log("Cancelled."); + return; + } + } + + const spinner = createSpinner(`Uninstalling ${trimmed}`); + try { + const target = join(installDir, trimmed); + + await rm(target, { recursive: true, force: true }); + + delete lock.skills[trimmed]; + await writeLockfile(installWorkdir, lock); + + const agentSuffix4 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; + spinner.succeed(`Uninstalled ${trimmed}${agentSuffix4}`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +type ExploreSort = "newest" | "downloads" | "rating" | "installs" | "installsAllTime" | "trending"; +type ApiExploreSort = + | "createdAt" + | "updated" + | "downloads" + | "stars" + | "installsCurrent" + | "installsAllTime" + | "trending"; + +export async function cmdExplore( + opts: GlobalOpts, + options: { limit?: number; sort?: string; json?: boolean } = {}, +) { + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Fetching latest skills"); + try { + const url = registryUrl(ApiRoutes.skills, registry); + const boundedLimit = clampLimit(options.limit ?? 25); + const { apiSort } = resolveExploreSort(options.sort); + url.searchParams.set("limit", String(boundedLimit)); + if (apiSort !== "updated") url.searchParams.set("sort", apiSort); + const result = await apiRequest( + registry, + { method: "GET", url: url.toString() }, + ApiV1SkillListResponseSchema, + ); + + spinner.stop(); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + return; + } + if (result.items.length === 0) { + console.log("No skills found."); + return; + } + + for (const item of result.items) { + console.log(formatExploreLine(item)); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export function formatExploreLine(item: { + slug: string; + summary?: string | null; + updatedAt: number; + latestVersion?: { version: string } | null; +}) { + const version = item.latestVersion?.version ?? "?"; + const age = formatRelativeTime(item.updatedAt); + const summary = item.summary ? ` ${truncate(item.summary, 50)}` : ""; + return `${item.slug} v${version} ${age}${summary}`; +} + +export function clampLimit(limit: number, fallback = 25) { + if (!Number.isFinite(limit)) return fallback; + return Math.min(Math.max(1, limit), 200); +} + +export async function cmdReportSkill( + opts: GlobalOpts, + slug: string, + options: SkillReportOptions = {}, +) { + const trimmed = normalizeSkillSlugOrFail(slug); + const reason = options.reason?.trim(); + if (!reason) fail("--reason required"); + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/report`, + token, + body: { + reason, + ...(options.version?.trim() ? { version: options.version.trim() } : {}), + }, + }, + ApiV1SkillReportResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + if (result.alreadyReported) { + console.log(`Already reported ${trimmed}.`); + } else { + console.log(`OK. Reported ${trimmed} (${result.reportId}).`); + } +} + +export async function cmdListSkillReports(opts: GlobalOpts, options: SkillReportListOptions = {}) { + const status = options.status?.trim() || "open"; + if (!["open", "confirmed", "dismissed", "all"].includes(status)) { + fail("--status must be open, confirmed, dismissed, or all"); + } + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const url = registryUrl(`${ApiRoutes.skills}/-/reports`, registry); + url.searchParams.set("status", status); + if (options.cursor?.trim()) url.searchParams.set("cursor", options.cursor.trim()); + url.searchParams.set("limit", String(clampLimit(options.limit ?? 25, 25))); + const result = await apiRequest( + registry, + { method: "GET", url: url.toString(), token }, + ApiV1SkillReportListResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + if (result.items.length === 0) { + console.log("No skill reports found."); + } else { + for (const item of result.items) { + const reporter = item.reporter.handle ?? item.reporter.userId; + console.log(`${item.reportId} ${item.status} ${item.slug}`); + console.log(` reporter: ${reporter}`); + if (item.reason) console.log(` reason: ${item.reason}`); + if (item.triageNote) console.log(` note: ${item.triageNote}`); + } + } + if (!result.done && result.nextCursor) console.log(`Next cursor: ${result.nextCursor}`); +} + +export async function cmdTriageSkillReport( + opts: GlobalOpts, + reportId: string, + options: SkillReportTriageOptions = {}, +) { + const trimmed = reportId.trim(); + if (!trimmed) fail("Report id required"); + const statusValue = options.status?.trim(); + if (!statusValue || !["open", "confirmed", "dismissed"].includes(statusValue)) { + fail("--status must be open, confirmed, or dismissed"); + } + const status = statusValue as SkillReportStatus; + const finalAction = (options.finalAction ?? options.action)?.trim() as + | SkillReportFinalAction + | undefined; + if (finalAction && !["none", "hide"].includes(finalAction)) { + fail("--action must be none or hide"); + } + const note = options.note?.trim(); + if (status !== "open" && !note) fail("--note required unless reopening"); + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + await presentModerationPlan( + reportModerationPlan({ + entityLabel: "skill", + reportId: trimmed, + status, + finalAction: finalAction ?? "none", + }), + options, + ); + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/-/reports/${encodeURIComponent(trimmed)}/triage`, + token, + body: { + status, + ...(note ? { note } : {}), + ...(finalAction ? { finalAction } : {}), + }, + }, + ApiV1SkillReportTriageResponseSchema, + ); + + if (options.json) { + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } + const actionSuffix = + result.actionTaken && result.actionTaken !== "none" ? `; action ${result.actionTaken}` : ""; + console.log(`OK. Skill report ${trimmed} set to ${result.status}${actionSuffix}.`); +} + +function formatRelativeTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 30) { + const months = Math.floor(days / 30); + return `${months}mo ago`; + } + if (days > 0) return `${days}d ago`; + if (hours > 0) return `${hours}h ago`; + if (minutes > 0) return `${minutes}m ago`; + return "just now"; +} + +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return `${str.slice(0, maxLen - 1)}…`; +} + +function resolveExploreSort(raw?: string): { sort: ExploreSort; apiSort: ApiExploreSort } { + const normalized = raw?.trim().toLowerCase(); + if ( + !normalized || + normalized === "newest" || + normalized === "createdat" || + normalized === "created-at" + ) { + return { sort: "newest", apiSort: "createdAt" }; + } + if (normalized === "updated") { + return { sort: "newest", apiSort: "updated" }; + } + if (normalized === "downloads" || normalized === "download") { + return { sort: "downloads", apiSort: "downloads" }; + } + if (normalized === "rating" || normalized === "stars" || normalized === "star") { + return { sort: "rating", apiSort: "stars" }; + } + if ( + normalized === "installs" || + normalized === "install" || + normalized === "installscurrent" || + normalized === "installs-current" || + normalized === "current" + ) { + return { sort: "installs", apiSort: "installsCurrent" }; + } + if (normalized === "installsalltime" || normalized === "installs-all-time") { + return { sort: "installsAllTime", apiSort: "installsAllTime" }; + } + if (normalized === "trending") { + return { sort: "trending", apiSort: "trending" }; + } + return fail( + `Invalid sort "${raw}". Use newest, updated, downloads, rating, installs, installsAllTime, or trending.`, + ); +} + +async function resolveSkillVersion(registry: string, slug: string, hash: string): Promise { + const url = registryUrl(ApiRoutes.resolve, registry); + url.searchParams.set("slug", slug); + url.searchParams.set("hash", hash); + return apiRequest( + registry, + { method: "GET", url: url.toString() }, + ApiV1SkillResolveResponseSchema, + ); +} + +async function fileExists(path: string) { + try { + await stat(path); + return true; + } catch { + return false; + } +} diff --git a/dt-skill/src/cli/commands/star.ts b/dt-skill/src/cli/commands/star.ts new file mode 100644 index 00000000..95a596c8 --- /dev/null +++ b/dt-skill/src/cli/commands/star.ts @@ -0,0 +1,37 @@ +import { apiRequest } from "../../http.js"; +import { ApiRoutes, ApiV1StarResponseSchema } from "../../schema/index.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; + +export async function cmdStarSkill( + opts: GlobalOpts, + slugArg: string, + options: { yes?: boolean }, + inputAllowed: boolean, +) { + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail("Slug required"); + const allowPrompt = isInteractive() && inputAllowed !== false; + + if (!options.yes) { + if (!allowPrompt) fail("Pass --yes (no input)"); + const ok = await promptConfirm(`Star ${slug}?`); + if (!ok) return undefined; + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Starring ${slug}`); + try { + const result = await apiRequest( + registry, + { method: "POST", path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}` }, + ApiV1StarResponseSchema, + ); + spinner.succeed(result.alreadyStarred ? `OK. ${slug} already starred.` : `OK. Starred ${slug}`); + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} diff --git a/dt-skill/src/cli/commands/sync.test.ts b/dt-skill/src/cli/commands/sync.test.ts new file mode 100644 index 00000000..14aac957 --- /dev/null +++ b/dt-skill/src/cli/commands/sync.test.ts @@ -0,0 +1,555 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; + +const mockIntro = vi.fn(); +const mockOutro = vi.fn(); +const mockLog = vi.fn(); +const mockMultiselect = vi.fn(async (_args?: unknown) => [] as string[]); +let interactive = false; +const mocked = (value: T) => + value as T & { mockImplementation: (...args: unknown[]) => unknown }; + +const defaultFindSkillFolders = async (root: string) => { + if (!root.endsWith("/scan")) return []; + return [ + { folder: "/scan/new-skill", slug: "new-skill", displayName: "New Skill" }, + { folder: "/scan/synced-skill", slug: "synced-skill", displayName: "Synced Skill" }, + { folder: "/scan/update-skill", slug: "update-skill", displayName: "Update Skill" }, + ]; +}; + +vi.mock("@clack/prompts", () => ({ + intro: (value: string) => mockIntro(value), + outro: (value: string) => mockOutro(value), + multiselect: (args: unknown) => mockMultiselect(args), + text: vi.fn(async () => ""), + isCancel: () => false, +})); + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); +httpMocks.downloadZip.mockImplementation( + async (_registry?: unknown, _args?: unknown) => new Uint8Array([1, 2, 3]), +); +const mockApiRequest = httpMocks.apiRequest; +const mockFail = uiMocks.fail; +const mockSpinner = uiMocks.spinner; +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../ui.js", () => ({ + createSpinner: vi.fn(() => mockSpinner), + fail: (message: string) => mockFail(message), + formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), + isInteractive: () => interactive, + promptConfirm: uiMocks.promptConfirm, +})); + +vi.mock("../scanSkills.js", () => ({ + findSkillFolders: vi.fn(defaultFindSkillFolders), + getFallbackSkillRoots: vi.fn(() => []), +})); + +const mockResolveClawdbotSkillRoots = vi.fn( + async () => + ({ + roots: [] as string[], + labels: {} as Record, + }) as const, +); +vi.mock("../clawdbotConfig.js", () => ({ + resolveClawdbotSkillRoots: () => mockResolveClawdbotSkillRoots(), +})); + +const mockListTextFiles = vi.fn(async (folder: string) => [ + { relPath: "SKILL.md", bytes: new TextEncoder().encode(folder) }, +]); +const mockHashSkillFiles = vi.fn((files: Array<{ relPath: string; bytes: Uint8Array }>) => ({ + fingerprint: files + .map((file) => `${file.relPath}:${Buffer.from(file.bytes).toString("hex")}`) + .join("|"), + files: [], +})); +const mockHashSkillZip = vi.fn((_zip?: Uint8Array) => ({ + fingerprint: "remote-fingerprint", + files: [], +})); +const mockReadSkillOrigin = vi.fn(async (_folder?: string) => null); +vi.mock("../../skills.js", () => ({ + listTextFiles: (folder: string) => mockListTextFiles(folder), + hashSkillFiles: (files: Array<{ relPath: string; bytes: Uint8Array }>) => + mockHashSkillFiles(files), + hashSkillZip: (zip: Uint8Array) => mockHashSkillZip(zip), + readSkillOrigin: (folder: string) => mockReadSkillOrigin(folder), +})); + +const mockCmdPublish = vi.fn(); +vi.mock("./publish.js", () => ({ + cmdPublish: (opts: unknown, folder: unknown, options?: unknown) => + mockCmdPublish(opts, folder, options), +})); + +const { cmdSync } = await import("./sync"); + +function makeOpts() { + return makeGlobalOpts(); +} + +afterEach(async () => { + vi.clearAllMocks(); + mockCmdPublish.mockReset(); + process.exitCode = undefined; + const { findSkillFolders } = await import("../scanSkills.js"); + mocked(findSkillFolders).mockImplementation(defaultFindSkillFolders); +}); + +vi.spyOn(console, "log").mockImplementation((...args) => { + mockLog(args.map(String).join(" ")); +}); + +describe("cmdSync", () => { + it("classifies skills as new/update/synced (dry-run, mocked HTTP)", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: true }, true); + + expect(mockCmdPublish).not.toHaveBeenCalled(); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Already synced/); + expect(output).toMatch(/synced-skill/); + + const dryRunOutro = mockOutro.mock.calls.at(-1)?.[0]; + expect(String(dryRunOutro)).toMatch(/Dry run: would upload 2 skill/); + }); + + it("prints bullet lists and selects all actionable by default", async () => { + interactive = true; + mockMultiselect.mockImplementation(async (args?: unknown) => { + const { initialValues } = args as { initialValues: string[] }; + return initialValues; + }); + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: false, dryRun: false, bump: "patch" }, true); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/To sync/); + expect(output).toMatch(/- new-skill/); + expect(output).toMatch(/- update-skill/); + expect(output).toMatch(/Already synced/); + expect(output).toMatch(/- synced-skill/); + + const lastCall = mockMultiselect.mock.calls.at(-1); + const promptArgs = lastCall ? (lastCall[0] as { initialValues: string[] }) : undefined; + expect(promptArgs?.initialValues.length).toBe(2); + expect(mockCmdPublish).toHaveBeenCalledTimes(2); + }); + + it("labels unmatched local content as proposed publish versions, not registry updates", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: true }, true); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/update-skill\s+LOCAL CHANGES latest 1\.0\.0; publish 1\.0\.1/); + expect(output).toMatch(/new-skill\s+NEW \(publish 1\.0\.0\)/); + expect(output).not.toMatch(/UPDATE 1\.0\.0/); + }); + + it("shows condensed synced list when nothing to sync", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + return { match: { version: "1.0.0" }, latestVersion: { version: "1.0.0" } }; + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false }, true); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Already synced/); + expect(output).toMatch(/new-skill@1.0.0/); + expect(output).toMatch(/synced-skill@1.0.0/); + expect(output).not.toMatch(/\n-/); + + const outro = mockOutro.mock.calls.at(-1)?.[0]; + expect(String(outro)).toMatch(/Nothing to sync/); + }); + + it("dedupes duplicate slugs before publishing", async () => { + interactive = false; + const { findSkillFolders } = await import("../scanSkills.js"); + mocked(findSkillFolders).mockImplementation(async (root: string) => { + if (!root.endsWith("/scan")) return []; + return [ + { folder: "/scan/dup-skill", slug: "dup-skill", displayName: "Dup Skill" }, + { folder: "/scan/dup-skill-copy", slug: "dup-skill", displayName: "Dup Skill" }, + ]; + }); + + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + return { match: null, latestVersion: null }; + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false }, true); + + expect(mockCmdPublish).toHaveBeenCalledTimes(1); + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Skipped duplicate slugs/); + expect(output).toMatch(/dup-skill/); + }); + + it("prints labeled roots when clawdbot roots are detected", async () => { + interactive = false; + mockResolveClawdbotSkillRoots.mockResolvedValueOnce({ + roots: ["/auto"], + labels: { "/auto": "Agent: Work" }, + }); + const { findSkillFolders } = await import("../scanSkills.js"); + mocked(findSkillFolders).mockImplementation(async (root: string) => { + if (root === "/auto") { + return [{ folder: "/auto/alpha", slug: "alpha", displayName: "Alpha" }]; + } + return []; + }); + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + throw new Error("Skill not found"); + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { all: true, dryRun: true }, true); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Roots with skills/); + expect(output).toMatch(/Agent: Work/); + }); + + it("allows empty changelog for updates (interactive)", async () => { + interactive = true; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); + + const calls = mockCmdPublish.mock.calls.map( + (call) => call[2] as { slug: string; changelog: string }, + ); + const update = calls.find((c) => c.slug === "update-skill"); + if (!update) throw new Error("Missing update-skill publish"); + expect(update.changelog).toBe(""); + }); + + it("continues uploading after a publish failure", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + mockCmdPublish.mockImplementation(async (_opts, _folder, options?: unknown) => { + const { slug } = options as { slug: string }; + if (slug === "new-skill") { + throw new Error("Registry rejected upload"); + } + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); + + expect(mockCmdPublish).toHaveBeenCalledTimes(2); + expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ + "new-skill", + "update-skill", + ]); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Failed to upload/); + expect(output).toMatch(/new-skill/); + expect(output).toMatch(/Registry rejected upload/); + + const outro = mockOutro.mock.calls.at(-1)?.[0]; + expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); + expect(process.exitCode).toBe(1); + }); + + it("continues uploading after an alias slug conflict publish failure", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + mockCmdPublish.mockImplementation(async (_opts, _folder, options?: unknown) => { + const { slug } = options as { slug: string }; + if (slug === "new-skill") { + throw new Error( + "Slug redirects to an existing skill. Choose a different slug. Existing skill: /alice/demo", + ); + } + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); + + expect(mockCmdPublish).toHaveBeenCalledTimes(2); + expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ + "new-skill", + "update-skill", + ]); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Failed to upload/); + expect(output).toMatch(/Slug redirects to an existing skill/); + expect(output).toMatch(/Existing skill: \/alice\/demo/); + + const outro = mockOutro.mock.calls.at(-1)?.[0]; + expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); + expect(process.exitCode).toBe(1); + }); + + it("continues uploading after a locked slug publish failure", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + mockCmdPublish.mockImplementation(async (_opts, _folder, options?: unknown) => { + const { slug } = options as { slug: string }; + if (slug === "new-skill") { + throw new Error( + "This slug is locked to a deleted or banned account. If you believe you are the rightful owner, please contact security@openclaw.ai to reclaim it.", + ); + } + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); + + expect(mockCmdPublish).toHaveBeenCalledTimes(2); + expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ + "new-skill", + "update-skill", + ]); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Failed to upload/); + expect(output).toMatch(/This slug is locked to a deleted or banned account/); + + const outro = mockOutro.mock.calls.at(-1)?.[0]; + expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); + expect(process.exitCode).toBe(1); + }); + + it("records unrelated publish failures as per-skill failures", async () => { + interactive = false; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + const u = new URL(`https://x.test${args.path}`); + const slug = u.searchParams.get("slug"); + if (slug === "new-skill") { + throw new Error("Skill not found"); + } + if (slug === "synced-skill") { + return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; + } + if (slug === "update-skill") { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + mockCmdPublish.mockRejectedValueOnce(new Error("HTTP 500")); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); + + expect(mockCmdPublish).toHaveBeenCalledTimes(2); + expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ + "new-skill", + "update-skill", + ]); + + const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); + expect(output).toMatch(/Failed to upload/); + expect(output).toMatch(/new-skill: HTTP 500/); + + const outro = mockOutro.mock.calls.at(-1)?.[0]; + expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); + expect(process.exitCode).toBe(1); + }); + + it("aborts command-level failures before publishing", async () => { + interactive = false; + const { findSkillFolders } = await import("../scanSkills.js"); + mocked(findSkillFolders).mockImplementation(async (root: string) => { + if (!root.endsWith("/scan")) return []; + return [{ folder: "/scan/update-skill", slug: "update-skill", displayName: "Update Skill" }]; + }); + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path === "/api/cli/telemetry/sync") return { ok: true }; + if (args.path.startsWith("/api/v1/resolve?")) { + return { match: null, latestVersion: { version: "1.0.0" } }; + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await expect( + cmdSync( + makeOpts(), + { root: ["/scan"], all: true, dryRun: false, bump: "not-semver" as never }, + true, + ), + ).rejects.toThrow("Could not bump version for update-skill"); + + expect(mockCmdPublish).not.toHaveBeenCalled(); + }); + + it("skips telemetry when CLAWHUB_DISABLE_TELEMETRY is set", async () => { + interactive = false; + process.env.CLAWHUB_DISABLE_TELEMETRY = "1"; + mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { + if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; + if (args.path.startsWith("/api/v1/resolve?")) { + return { match: { version: "1.0.0" }, latestVersion: { version: "1.0.0" } }; + } + throw new Error(`Unexpected apiRequest: ${args.path}`); + }); + + await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: true }, true); + expect( + mockApiRequest.mock.calls.some((call) => call[1]?.path === "/api/cli/telemetry/sync"), + ).toBe(false); + delete process.env.CLAWHUB_DISABLE_TELEMETRY; + }); +}); diff --git a/dt-skill/src/cli/commands/sync.ts b/dt-skill/src/cli/commands/sync.ts new file mode 100644 index 00000000..7ae4311c --- /dev/null +++ b/dt-skill/src/cli/commands/sync.ts @@ -0,0 +1,214 @@ +import { intro, outro } from "@clack/prompts"; +import { hashSkillFiles, listTextFiles, readSkillOrigin } from "../../skills.js"; +import { resolveClawdbotSkillRoots } from "../clawdbotConfig.js"; +import { getFallbackSkillRoots } from "../scanSkills.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive } from "../ui.js"; +import { cmdPublish } from "./publish.js"; +import { + buildScanRoots, + checkRegistrySyncState, + dedupeSkillsBySlug, + formatActionableLine, + formatBulletList, + formatCommaList, + formatList, + formatSyncedDisplay, + formatSyncedSummary, + getRegistryForSync, + mapWithConcurrency, + mergeScan, + normalizeConcurrency, + printSection, + reportTelemetryIfEnabled, + resolvePublishMeta, + scanRootsWithLabels, + selectToUpload, +} from "./syncHelpers.js"; +import type { Candidate, LocalSkill, SyncOptions } from "./syncTypes.js"; + +export async function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllowed: boolean) { + const allowPrompt = isInteractive() && inputAllowed !== false; + intro("ClawHub sync"); + + const registry = await getRegistryForSync(opts); + const selectedRoots = buildScanRoots(opts, options.root); + const clawdbotRoots = await resolveClawdbotSkillRoots(); + const combinedRoots = Array.from( + new Set([...selectedRoots, ...clawdbotRoots.roots].map((root) => root.trim()).filter(Boolean)), + ); + const concurrency = normalizeConcurrency(options.concurrency); + + const spinner = createSpinner("Scanning for local skills"); + const primaryScan = await scanRootsWithLabels(combinedRoots, clawdbotRoots.labels); + let scan = primaryScan; + let telemetryScan = primaryScan; + if (primaryScan.skills.length === 0) { + const fallback = getFallbackSkillRoots(opts.workdir); + const fallbackScan = await scanRootsWithLabels(fallback); + spinner.stop(); + telemetryScan = mergeScan(primaryScan, fallbackScan); + scan = fallbackScan; + if (fallbackScan.skills.length === 0) + fail("No skills found (checked workdir and known Clawdis/Clawd locations)"); + printSection( + `No skills in workdir. Found ${fallbackScan.skills.length} in fallback locations.`, + formatList(fallbackScan.rootsWithSkills, 10), + ); + } else { + spinner.stop(); + const labeledRoots = primaryScan.rootsWithSkills + .map((root) => { + const label = primaryScan.rootLabels?.[root]; + return label ? `${label} (${root})` : root; + }) + .filter(Boolean); + if (labeledRoots.length > 0) { + printSection("Roots with skills", formatList(labeledRoots, 10)); + } + } + const deduped = dedupeSkillsBySlug(scan.skills); + const skills = deduped.skills; + if (deduped.duplicates.length > 0) { + printSection("Skipped duplicate slugs", formatCommaList(deduped.duplicates, 16)); + } + const parsingSpinner = createSpinner("Parsing local skills"); + const locals: LocalSkill[] = []; + try { + let done = 0; + const parsed = await mapWithConcurrency(skills, Math.min(concurrency, 12), async (skill) => { + const filesOnDisk = await listTextFiles(skill.folder); + const hashed = hashSkillFiles(filesOnDisk); + const origin = await readSkillOrigin(skill.folder); + done += 1; + parsingSpinner.text = `Parsing local skills ${done}/${skills.length}`; + return { + ...skill, + fingerprint: hashed.fingerprint, + fileCount: filesOnDisk.length, + origin, + }; + }); + locals.push(...parsed); + } catch (error) { + parsingSpinner.fail(formatError(error)); + throw error; + } finally { + parsingSpinner.stop(); + } + + const candidatesSpinner = createSpinner("Checking registry sync state"); + const candidates: Candidate[] = []; + const resolveSupport: { value: boolean | null } = { value: null }; + try { + let done = 0; + const resolved = await mapWithConcurrency(locals, Math.min(concurrency, 16), async (skill) => { + try { + return await checkRegistrySyncState(registry, skill, resolveSupport); + } finally { + done += 1; + candidatesSpinner.text = `Checking registry sync state ${done}/${locals.length}`; + } + }); + candidates.push(...resolved); + } catch (error) { + candidatesSpinner.fail(formatError(error)); + throw error; + } finally { + candidatesSpinner.stop(); + } + + await reportTelemetryIfEnabled({ + registry, + scan: telemetryScan, + candidates, + }); + + const synced = candidates.filter((candidate) => candidate.status === "synced"); + const actionable = candidates.filter((candidate) => candidate.status !== "synced"); + const bump = options.bump ?? "patch"; + + if (actionable.length === 0) { + if (synced.length > 0) { + printSection("Already synced", formatCommaList(synced.map(formatSyncedSummary), 16)); + } + outro("Nothing to sync."); + return; + } + + printSection( + "To sync", + formatBulletList( + actionable.map((candidate) => formatActionableLine(candidate, bump)), + 20, + ), + ); + if (synced.length > 0) { + printSection("Already synced", formatSyncedDisplay(synced)); + } + + const selected = await selectToUpload(actionable, { + allowPrompt, + all: Boolean(options.all), + bump, + }); + if (selected.length === 0) { + outro("Nothing selected."); + return; + } + + if (options.dryRun) { + outro(`Dry run: would upload ${selected.length} skill(s).`); + return; + } + + const tags = options.tags ?? "latest"; + const failedUploads: Array<{ slug: string; message: string }> = []; + let uploaded = 0; + + for (const skill of selected) { + const { publishVersion, changelog } = await resolvePublishMeta(skill, { + bump, + allowPrompt, + changelogFlag: options.changelog, + }); + const forkOf = + skill.origin && normalizeRegistry(skill.origin.registry) === normalizeRegistry(registry) + ? skill.origin.slug !== skill.slug + ? `${skill.origin.slug}@${skill.origin.installedVersion}` + : undefined + : undefined; + try { + await cmdPublish(opts, skill.folder, { + slug: skill.slug, + name: skill.displayName, + version: publishVersion, + changelog, + tags, + forkOf, + }); + uploaded += 1; + } catch (error) { + failedUploads.push({ slug: skill.slug, message: formatError(error) }); + } + } + + if (failedUploads.length > 0) { + printSection( + "Failed to upload", + formatBulletList( + failedUploads.map((failure) => `${failure.slug}: ${failure.message}`), + 20, + ), + ); + outro(`Uploaded ${uploaded} of ${selected.length} skill(s). ${failedUploads.length} failed.`); + process.exitCode = 1; + return; + } + + outro(`Uploaded ${selected.length} skill(s).`); +} + +function normalizeRegistry(value: string) { + return value.trim().replace(/\/+$/, "").toLowerCase(); +} diff --git a/dt-skill/src/cli/commands/syncHelpers.test.ts b/dt-skill/src/cli/commands/syncHelpers.test.ts new file mode 100644 index 00000000..794753cc --- /dev/null +++ b/dt-skill/src/cli/commands/syncHelpers.test.ts @@ -0,0 +1,26 @@ +/* @vitest-environment node */ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../scanSkills.js", () => ({ + findSkillFolders: vi.fn(async (root: string) => { + if (root.endsWith("/with-skill")) { + return [{ folder: `${root}/demo`, slug: "demo", displayName: "Demo" }]; + } + return []; + }), +})); + +const { scanRootsWithLabels } = await import("./syncHelpers.js"); + +describe("scanRootsWithLabels", () => { + it("attaches labels to roots with skills", async () => { + const roots = ["/tmp/with-skill", "/tmp/empty", "/tmp/with-skill"]; + const labels = { "/tmp/with-skill": "Agent: Work" }; + + const result = await scanRootsWithLabels(roots, labels); + + expect(result.rootsWithSkills).toEqual(["/tmp/with-skill"]); + expect(result.rootLabels).toEqual({ "/tmp/with-skill": "Agent: Work" }); + expect(result.skills.map((skill) => skill.slug)).toEqual(["demo"]); + }); +}); diff --git a/dt-skill/src/cli/commands/syncHelpers.ts b/dt-skill/src/cli/commands/syncHelpers.ts new file mode 100644 index 00000000..e48ed3a2 --- /dev/null +++ b/dt-skill/src/cli/commands/syncHelpers.ts @@ -0,0 +1,405 @@ +import { createHash } from "node:crypto"; +import { realpath } from "node:fs/promises"; +import { resolve } from "node:path"; +import { isCancel, multiselect } from "@clack/prompts"; +import semver from "semver"; +import { resolveHome } from "../../homedir.js"; +import { apiRequest, downloadZip } from "../../http.js"; +import { + ApiCliTelemetrySyncResponseSchema, + ApiRoutes, + ApiV1SkillResolveResponseSchema, + ApiV1SkillResponseSchema, + LegacyApiRoutes, +} from "../../schema/index.js"; +import { hashSkillZip } from "../../skills.js"; +import { getRegistry } from "../registry.js"; +import { findSkillFolders, type SkillFolder } from "../scanSkills.js"; +import type { GlobalOpts } from "../types.js"; +import { fail, formatError } from "../ui.js"; +import type { Candidate, LocalSkill } from "./syncTypes.js"; + +export async function reportTelemetryIfEnabled(params: { + registry: string; + scan: { roots: string[]; skillsByRoot: Record }; + candidates: Candidate[]; +}) { + if (isTelemetryDisabled()) return; + const versionBySlug = new Map(); + for (const candidate of params.candidates) { + versionBySlug.set(candidate.slug, candidate.matchVersion ?? null); + } + + const roots = params.scan.roots.map((root) => ({ + rootId: rootTelemetryId(root), + label: formatRootLabel(root), + skills: (params.scan.skillsByRoot[root] ?? []).map((skill) => ({ + slug: skill.slug, + version: versionBySlug.get(skill.slug) ?? null, + })), + })); + + try { + await apiRequest( + params.registry, + { + method: "POST", + path: LegacyApiRoutes.cliTelemetrySync, + body: { roots }, + }, + ApiCliTelemetrySyncResponseSchema, + ); + } catch { + // ignore telemetry failures + } +} + +function isTelemetryDisabled() { + const raw = process.env.CLAWHUB_DISABLE_TELEMETRY ?? process.env.CLAWDHUB_DISABLE_TELEMETRY; + if (!raw) return false; + return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase()); +} + +export function buildScanRoots(opts: GlobalOpts, extraRoots: string[] | undefined) { + const roots = [opts.workdir, opts.dir, ...(extraRoots ?? [])]; + return Array.from(new Set(roots.map((root) => resolve(root)))); +} + +export function normalizeConcurrency(value: number | undefined) { + const raw = typeof value === "number" ? value : 4; + const rounded = Number.isFinite(raw) ? Math.round(raw) : 4; + return Math.min(32, Math.max(1, rounded)); +} + +export async function mapWithConcurrency( + items: T[], + limit: number, + fn: (item: T) => Promise, +) { + const results = Array.from({ length: items.length }) as R[]; + let nextIndex = 0; + const workerCount = Math.min(Math.max(1, limit), items.length || 1); + + async function worker() { + while (true) { + const index = nextIndex; + nextIndex += 1; + if (index >= items.length) return; + results[index] = await fn(items[index] as T); + } + } + + await Promise.all(Array.from({ length: workerCount }, () => worker())); + return results; +} + +export async function checkRegistrySyncState( + registry: string, + skill: LocalSkill, + resolveSupport: { value: boolean | null }, +): Promise { + if (resolveSupport.value !== false) { + try { + const resolved = await apiRequest( + registry, + { + method: "GET", + path: `${ApiRoutes.resolve}?slug=${encodeURIComponent(skill.slug)}&hash=${encodeURIComponent(skill.fingerprint)}`, + }, + ApiV1SkillResolveResponseSchema, + ); + resolveSupport.value = true; + const latestVersion = resolved.latestVersion?.version ?? null; + const matchVersion = resolved.match?.version ?? null; + if (!latestVersion) { + return { + ...skill, + status: "new", + matchVersion: null, + latestVersion: null, + }; + } + return { + ...skill, + status: matchVersion ? "synced" : "update", + matchVersion, + latestVersion, + }; + } catch (error) { + const message = formatError(error); + if (/skill not found/i.test(message) || /HTTP 404/i.test(message)) { + resolveSupport.value = true; + return { + ...skill, + status: "new", + matchVersion: null, + latestVersion: null, + }; + } + if (/no matching routes found/i.test(message)) { + resolveSupport.value = false; + } else { + throw error; + } + } + } + + const meta = await apiRequest( + registry, + { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(skill.slug)}` }, + ApiV1SkillResponseSchema, + ).catch(() => null); + + const latestVersion = meta?.latestVersion?.version ?? null; + if (!latestVersion) { + return { + ...skill, + status: "new", + matchVersion: null, + latestVersion: null, + }; + } + + const zip = await downloadZip(registry, { slug: skill.slug, version: latestVersion }); + const remote = hashSkillZip(zip).fingerprint; + const matchVersion = remote === skill.fingerprint ? latestVersion : null; + + return { + ...skill, + status: matchVersion ? "synced" : "update", + matchVersion, + latestVersion, + }; +} + +export async function scanRootsWithLabels(roots: string[], labels?: Record) { + const all: SkillFolder[] = []; + const rootsWithSkills: string[] = []; + const uniqueRoots = await dedupeRoots(roots); + const skillsByRoot: Record = {}; + const rootLabels: Record = {}; + for (const root of uniqueRoots) { + const found = await findSkillFolders(root); + skillsByRoot[root] = found; + if (found.length > 0) rootsWithSkills.push(root); + all.push(...found); + if (labels?.[root]) rootLabels[root] = labels[root] as string; + } + const byFolder = new Map(); + for (const folder of all) { + byFolder.set(folder.folder, folder); + } + return { + roots: uniqueRoots, + skillsByRoot, + skills: Array.from(byFolder.values()), + rootsWithSkills, + rootLabels, + }; +} + +export function mergeScan( + left: { + roots: string[]; + skillsByRoot: Record; + skills: SkillFolder[]; + rootsWithSkills: string[]; + rootLabels: Record; + }, + right: { + roots: string[]; + skillsByRoot: Record; + skills: SkillFolder[]; + rootsWithSkills: string[]; + rootLabels: Record; + }, +) { + const mergedRoots = Array.from(new Set([...left.roots, ...right.roots])); + const skillsByRoot: Record = {}; + for (const root of mergedRoots) { + skillsByRoot[root] = right.skillsByRoot[root] ?? left.skillsByRoot[root] ?? []; + } + const rootLabels: Record = { ...left.rootLabels, ...right.rootLabels }; + const byFolder = new Map(); + for (const entry of [...left.skills, ...right.skills]) { + byFolder.set(entry.folder, entry); + } + const skills = Array.from(byFolder.values()); + const rootsWithSkills = mergedRoots.filter((root) => (skillsByRoot[root]?.length ?? 0) > 0); + return { roots: mergedRoots, skillsByRoot, skills, rootsWithSkills, rootLabels }; +} + +async function dedupeRoots(roots: string[]) { + const seen = new Set(); + const unique: string[] = []; + for (const root of roots) { + const resolved = resolve(root); + const canonical = await realpath(resolved).catch(() => null); + const key = canonical ?? resolved; + if (seen.has(key)) continue; + seen.add(key); + unique.push(key); + } + return unique; +} + +export async function selectToUpload( + candidates: Candidate[], + params: { allowPrompt: boolean; all: boolean; bump: "patch" | "minor" | "major" }, +): Promise { + if (params.all || !params.allowPrompt) return candidates; + + const valueByKey = new Map(); + const choices = candidates.map((candidate) => { + const key = candidate.folder; + valueByKey.set(key, candidate); + return { + value: key, + label: `${candidate.slug} ${formatActionableStatus(candidate, params.bump)}`, + hint: `${abbreviatePath(candidate.folder)} | ${candidate.fileCount} files`, + }; + }); + + const picked = await multiselect({ + message: "Select skills to upload", + options: choices, + initialValues: choices.map((choice) => choice.value), + required: false, + }); + if (isCancel(picked)) fail("Canceled"); + const selected = picked.map((key) => valueByKey.get(key)).filter(Boolean) as Candidate[]; + return selected; +} + +export async function resolvePublishMeta( + skill: Candidate, + params: { bump: "patch" | "minor" | "major"; allowPrompt: boolean; changelogFlag?: string }, +) { + if (skill.status === "new") { + return { publishVersion: "1.0.0", changelog: "" }; + } + + const latest = skill.latestVersion; + if (!latest) fail(`Could not resolve latest version for ${skill.slug}`); + const publishVersion = semver.inc(latest, params.bump); + if (!publishVersion) fail(`Could not bump version for ${skill.slug}`); + + const fromFlag = params.changelogFlag?.trim(); + if (fromFlag) return { publishVersion, changelog: fromFlag }; + + return { publishVersion, changelog: "" }; +} + +export async function getRegistryForSync(opts: GlobalOpts) { + return getRegistry(opts, { cache: true }); +} + +export function formatList(values: string[], max: number) { + if (values.length === 0) return ""; + const shown = values.map(abbreviatePath); + if (shown.length <= max) return shown.join("\n"); + const head = shown.slice(0, Math.max(1, max - 1)); + const rest = values.length - head.length; + return [...head, `… +${rest} more`].join("\n"); +} + +export function printSection(title: string, body?: string) { + const trimmed = body?.trim(); + if (!trimmed) { + console.log(title); + return; + } + if (trimmed.includes("\n")) { + console.log(`\n${title}\n${trimmed}`); + return; + } + console.log(`${title}: ${trimmed}`); +} + +function abbreviatePath(value: string) { + const home = resolveHome(); + if (value.startsWith(home)) return `~${value.slice(home.length)}`; + return value; +} + +function rootTelemetryId(value: string) { + return createHash("sha256").update(value).digest("hex"); +} + +function formatRootLabel(value: string) { + const home = resolveHome(); + if (value === home) return "~"; + + const normalized = value.replaceAll("\\", "/"); + const normalizedHome = home.replaceAll("\\", "/"); + const isHome = normalized === normalizedHome || normalized.startsWith(`${normalizedHome}/`); + + const stripped = isHome ? normalized.slice(normalizedHome.length).replace(/^\//, "") : normalized; + const parts = stripped.split("/").filter(Boolean); + const tail = parts.slice(-2).join("/"); + + if (!tail) return isHome ? "~" : "…"; + return isHome ? `~/${tail}` : `…/${tail}`; +} + +export function dedupeSkillsBySlug(skills: SkillFolder[]) { + const bySlug = new Map(); + for (const skill of skills) { + const existing = bySlug.get(skill.slug); + if (existing) existing.push(skill); + else bySlug.set(skill.slug, [skill]); + } + const unique: SkillFolder[] = []; + const duplicates: string[] = []; + for (const [slug, entries] of bySlug.entries()) { + unique.push(entries[0] as SkillFolder); + if (entries.length > 1) duplicates.push(`${slug} (${entries.length})`); + } + return { skills: unique, duplicates }; +} + +function formatActionableStatus(candidate: Candidate, bump: "patch" | "minor" | "major"): string { + if (candidate.status === "new") return "NEW (publish 1.0.0)"; + const latest = candidate.latestVersion; + const next = latest ? semver.inc(latest, bump) : null; + if (latest && next) return `LOCAL CHANGES latest ${latest}; publish ${next}`; + return "LOCAL CHANGES"; +} + +export function formatActionableLine( + candidate: Candidate, + bump: "patch" | "minor" | "major", +): string { + return `${candidate.slug} ${formatActionableStatus(candidate, bump)} (${candidate.fileCount} files)`; +} + +function formatSyncedLine(candidate: Candidate): string { + const version = candidate.matchVersion ?? candidate.latestVersion ?? "unknown"; + return `${candidate.slug} synced (${version})`; +} + +export function formatSyncedSummary(candidate: Candidate): string { + const version = candidate.matchVersion ?? candidate.latestVersion; + return version ? `${candidate.slug}@${version}` : candidate.slug; +} + +export function formatBulletList(lines: string[], max: number): string { + if (lines.length <= max) return lines.map((line) => `- ${line}`).join("\n"); + const head = lines.slice(0, max); + const rest = lines.length - head.length; + return [...head, `... +${rest} more`].map((line) => `- ${line}`).join("\n"); +} + +export function formatSyncedDisplay(synced: Candidate[]) { + const lines = synced.map(formatSyncedLine); + if (lines.length <= 12) return formatBulletList(lines, 12); + return formatCommaList(synced.map(formatSyncedSummary), 24); +} + +export function formatCommaList(values: string[], max: number) { + if (values.length === 0) return ""; + if (values.length <= max) return values.join(", "); + const head = values.slice(0, Math.max(1, max - 1)); + const rest = values.length - head.length; + return `${head.join(", ")}, ... +${rest} more`; +} diff --git a/dt-skill/src/cli/commands/syncTypes.ts b/dt-skill/src/cli/commands/syncTypes.ts new file mode 100644 index 00000000..86948a2d --- /dev/null +++ b/dt-skill/src/cli/commands/syncTypes.ts @@ -0,0 +1,27 @@ +import type { SkillOrigin } from "../../skills.js"; +import type { SkillFolder } from "../scanSkills.js"; + +export type SyncOptions = { + root?: string[]; + all?: boolean; + dryRun?: boolean; + bump?: "patch" | "minor" | "major"; + changelog?: string; + tags?: string; + concurrency?: number; +}; + +export type Candidate = SkillFolder & { + fingerprint: string; + fileCount: number; + origin: SkillOrigin | null; + status: "synced" | "new" | "update"; + matchVersion: string | null; + latestVersion: string | null; +}; + +export type LocalSkill = SkillFolder & { + fingerprint: string; + fileCount: number; + origin: SkillOrigin | null; +}; diff --git a/dt-skill/src/cli/commands/transfer.test.ts b/dt-skill/src/cli/commands/transfer.test.ts new file mode 100644 index 00000000..00d5fc43 --- /dev/null +++ b/dt-skill/src/cli/commands/transfer.test.ts @@ -0,0 +1,132 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createAuthTokenModuleMocks, + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from "../../../test/cliCommandTestKit.js"; + +const authTokenMocks = createAuthTokenModuleMocks(); +const registryMocks = createRegistryModuleMocks(); +const httpMocks = createHttpModuleMocks(); +const uiMocks = createUiModuleMocks(); + +vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); +vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock("../../http.js", () => httpMocks.moduleFactory()); +vi.mock("../ui.js", () => uiMocks.moduleFactory()); + +const { + cmdTransferAccept, + cmdTransferCancel, + cmdTransferList, + cmdTransferReject, + cmdTransferRequest, +} = await import("./transfer"); + +const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe("transfer commands", () => { + it("request requires --yes when input is disabled", async () => { + await expect(cmdTransferRequest(makeGlobalOpts(), "demo", "@alice", {}, false)).rejects.toThrow( + /--yes/i, + ); + }); + + it("request calls transfer endpoint", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + transferId: "skillOwnershipTransfers:1", + toUserHandle: "alice", + expiresAt: Date.now() + 10_000, + }); + + await cmdTransferRequest( + makeGlobalOpts(), + "Demo", + "@Alice", + { yes: true, message: "Please take over" }, + false, + ); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "POST", + path: "/api/v1/skills/demo/transfer", + }), + expect.anything(), + ); + const requestArgs = httpMocks.apiRequest.mock.calls[0]?.[1] as { body?: unknown }; + expect(requestArgs.body).toEqual({ + toUserHandle: "alice", + message: "Please take over", + }); + }); + + it("list calls incoming transfers endpoint", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + transfers: [], + }); + await cmdTransferList(makeGlobalOpts(), {}); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "GET", + path: "/api/v1/transfers/incoming", + }), + expect.anything(), + ); + expect(consoleLog).toHaveBeenCalledWith("No incoming transfers."); + }); + + it("list supports outgoing endpoint", async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + transfers: [], + }); + await cmdTransferList(makeGlobalOpts(), { outgoing: true }); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: "GET", + path: "/api/v1/transfers/outgoing", + }), + expect.anything(), + ); + expect(consoleLog).toHaveBeenCalledWith("No outgoing transfers."); + }); + + it("accept/reject/cancel call action endpoints", async () => { + httpMocks.apiRequest.mockResolvedValue({ + ok: true, + skillSlug: "demo", + }); + + await cmdTransferAccept(makeGlobalOpts(), "demo", { yes: true }, false); + await cmdTransferReject(makeGlobalOpts(), "demo", { yes: true }, false); + await cmdTransferCancel(makeGlobalOpts(), "demo", { yes: true }, false); + + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/transfer/accept" }), + expect.anything(), + ); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/transfer/reject" }), + expect.anything(), + ); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/transfer/cancel" }), + expect.anything(), + ); + }); +}); diff --git a/dt-skill/src/cli/commands/transfer.ts b/dt-skill/src/cli/commands/transfer.ts new file mode 100644 index 00000000..b5a18f94 --- /dev/null +++ b/dt-skill/src/cli/commands/transfer.ts @@ -0,0 +1,217 @@ +import { apiRequest } from "../../http.js"; +import { + ApiRoutes, + ApiV1TransferDecisionResponseSchema, + ApiV1TransferListResponseSchema, + ApiV1TransferRequestResponseSchema, + parseArk, +} from "../../schema/index.js"; +import { requireAuthToken } from "../authToken.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; + +type ConfirmOptions = { yes?: boolean }; + +type DecisionAction = "accept" | "reject" | "cancel"; + +type DecisionSpec = { + verb: string; + progress: string; + success: string; + action: DecisionAction; +}; + +const DECISION_SPECS: Record = { + accept: { + verb: "Accept", + progress: "Accepting", + success: "Transfer accepted", + action: "accept", + }, + reject: { + verb: "Reject", + progress: "Rejecting", + success: "Transfer rejected", + action: "reject", + }, + cancel: { + verb: "Cancel", + progress: "Cancelling", + success: "Transfer cancelled", + action: "cancel", + }, +}; + +function normalizeSlug(slugArg: string) { + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail("Skill slug required"); + return slug; +} + +function canPrompt(inputAllowed: boolean) { + return isInteractive() && inputAllowed !== false; +} + +async function requireYesOrConfirm(options: ConfirmOptions, inputAllowed: boolean, prompt: string) { + if (options.yes) return true; + if (!canPrompt(inputAllowed)) fail("Pass --yes (no input)"); + return promptConfirm(prompt); +} + +export async function cmdTransferRequest( + opts: GlobalOpts, + slugArg: string, + toHandleArg: string, + options: ConfirmOptions & { message?: string }, + inputAllowed: boolean, +) { + const slug = normalizeSlug(slugArg); + const toHandle = toHandleArg.trim().replace(/^@+/, "").toLowerCase(); + if (!toHandle) fail("Recipient handle required (e.g., @username)"); + + const confirmed = await requireYesOrConfirm( + options, + inputAllowed, + `Transfer ${slug} to @${toHandle}? User transfers require recipient acceptance; org transfers apply immediately if you are an admin of both owners.`, + ); + if (!confirmed) return undefined; + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Requesting transfer of ${slug} to @${toHandle}`); + + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/transfer`, + token, + body: { + toUserHandle: toHandle, + message: options.message, + }, + }, + ApiV1TransferRequestResponseSchema, + ); + const parsed = parseArk( + ApiV1TransferRequestResponseSchema, + result, + "Transfer request response", + ); + if (parsed.transferred) { + spinner.succeed(`Transferred ${slug} to @${parsed.toPublisherHandle ?? toHandle}`); + } else { + spinner.succeed(`Transfer requested for ${slug} to @${parsed.toUserHandle ?? toHandle}`); + } + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export async function cmdTransferList(opts: GlobalOpts, options: { outgoing?: boolean }) { + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner("Fetching transfers"); + + try { + const path = options.outgoing + ? `${ApiRoutes.transfers}/outgoing` + : `${ApiRoutes.transfers}/incoming`; + const result = await apiRequest( + registry, + { method: "GET", path, token }, + ApiV1TransferListResponseSchema, + ); + const parsed = parseArk(ApiV1TransferListResponseSchema, result, "Transfer list response"); + spinner.stop(); + + if (parsed.transfers.length === 0) { + console.log(options.outgoing ? "No outgoing transfers." : "No incoming transfers."); + return parsed; + } + + console.log(options.outgoing ? "Outgoing transfers:" : "Incoming transfers:"); + for (const transfer of parsed.transfers) { + const otherHandle = options.outgoing ? transfer.toUser?.handle : transfer.fromUser?.handle; + const other = otherHandle ? `@${otherHandle.replace(/^@+/, "")}` : "(unknown user)"; + const expiresInDays = Math.max( + 0, + Math.ceil((transfer.expiresAt - Date.now()) / (24 * 60 * 60 * 1000)), + ); + console.log(` ${transfer.skill.slug} -> ${other} (expires in ${expiresInDays}d)`); + } + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +async function runTransferDecision( + opts: GlobalOpts, + slugArg: string, + options: ConfirmOptions, + inputAllowed: boolean, + spec: DecisionSpec, +) { + const slug = normalizeSlug(slugArg); + const confirmed = await requireYesOrConfirm( + options, + inputAllowed, + `${spec.verb} transfer of ${slug}?`, + ); + if (!confirmed) return undefined; + + const token = await requireAuthToken(); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`${spec.progress} transfer of ${slug}`); + + try { + const result = await apiRequest( + registry, + { + method: "POST", + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/transfer/${spec.action}`, + token, + }, + ApiV1TransferDecisionResponseSchema, + ); + const parsed = parseArk(ApiV1TransferDecisionResponseSchema, result, "Transfer response"); + spinner.succeed(`${spec.success} (${slug})`); + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} + +export function cmdTransferAccept( + opts: GlobalOpts, + slugArg: string, + options: ConfirmOptions, + inputAllowed: boolean, +) { + return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.accept); +} + +export function cmdTransferReject( + opts: GlobalOpts, + slugArg: string, + options: ConfirmOptions, + inputAllowed: boolean, +) { + return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.reject); +} + +export function cmdTransferCancel( + opts: GlobalOpts, + slugArg: string, + options: ConfirmOptions, + inputAllowed: boolean, +) { + return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.cancel); +} diff --git a/dt-skill/src/cli/commands/unstar.ts b/dt-skill/src/cli/commands/unstar.ts new file mode 100644 index 00000000..67d39b23 --- /dev/null +++ b/dt-skill/src/cli/commands/unstar.ts @@ -0,0 +1,39 @@ +import { apiRequest } from "../../http.js"; +import { ApiRoutes, ApiV1UnstarResponseSchema } from "../../schema/index.js"; +import { getRegistry } from "../registry.js"; +import type { GlobalOpts } from "../types.js"; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; + +export async function cmdUnstarSkill( + opts: GlobalOpts, + slugArg: string, + options: { yes?: boolean }, + inputAllowed: boolean, +) { + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail("Slug required"); + const allowPrompt = isInteractive() && inputAllowed !== false; + + if (!options.yes) { + if (!allowPrompt) fail("Pass --yes (no input)"); + const ok = await promptConfirm(`Unstar ${slug}?`); + if (!ok) return undefined; + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Unstarring ${slug}`); + try { + const result = await apiRequest( + registry, + { method: "DELETE", path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}` }, + ApiV1UnstarResponseSchema, + ); + spinner.succeed( + result.alreadyUnstarred ? `OK. ${slug} already unstarred.` : `OK. Unstarred ${slug}`, + ); + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} diff --git a/dt-skill/src/cli/helpStyle.ts b/dt-skill/src/cli/helpStyle.ts new file mode 100644 index 00000000..02fc10ba --- /dev/null +++ b/dt-skill/src/cli/helpStyle.ts @@ -0,0 +1,45 @@ +type Color = (value: string) => string; + +function wrap(start: string, end = "\x1b[0m"): Color { + return (value) => `${start}${value}${end}`; +} + +const ansi = { + reset: "\x1b[0m", + bold: wrap("\x1b[1m"), + dim: wrap("\x1b[2m"), + cyan: wrap("\x1b[36m"), + green: wrap("\x1b[32m"), + yellow: wrap("\x1b[33m"), +}; + +function isColorEnabled() { + if (!process.stdout.isTTY) return false; + if (process.env.NO_COLOR) return false; + return true; +} + +export function styleTitle(value: string) { + if (!isColorEnabled()) return value; + return `${ansi.bold(ansi.cyan(value))}${ansi.reset}`; +} + +export function configureCommanderHelp(program: { + configureHelp: (config: { + sectionTitle?: (title: string) => string; + optionTerm?: (option: { flags: string }) => string; + commandTerm?: (cmd: { name: () => string }) => string; + }) => unknown; +}) { + if (!isColorEnabled()) return; + program.configureHelp({ + sectionTitle: (title) => ansi.bold(ansi.cyan(title)), + optionTerm: (option) => ansi.yellow(option.flags), + commandTerm: (cmd) => ansi.green(cmd.name()), + }); +} + +export function styleEnvBlock(value: string) { + if (!isColorEnabled()) return value; + return `${ansi.dim(value)}${ansi.reset}`; +} diff --git a/dt-skill/src/cli/prompts/search-multiselect.ts b/dt-skill/src/cli/prompts/search-multiselect.ts new file mode 100644 index 00000000..3d47d17a --- /dev/null +++ b/dt-skill/src/cli/prompts/search-multiselect.ts @@ -0,0 +1,383 @@ +import * as readline from 'readline'; +import { stripVTControlCharacters } from 'node:util'; +import { Writable } from 'stream'; +import pc from 'picocolors'; + +// Silent writable stream to prevent readline from echoing input +const silentOutput = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, +}); + +export interface SearchItem { + value: T; + label: string; + hint?: string; +} + +export interface LockedSection { + title: string; + items: SearchItem[]; +} + +export interface SearchMultiselectOptions { + message: string; + items: SearchItem[]; + maxVisible?: number; + initialSelected?: T[]; + /** If true, require at least one item to be selected before submitting */ + required?: boolean; + /** Locked section shown above the searchable list - items are always selected and can't be toggled */ + lockedSection?: LockedSection; +} + +const S_STEP_ACTIVE = pc.green('◆'); +const S_STEP_CANCEL = pc.red('■'); +const S_STEP_SUBMIT = pc.green('◇'); +const S_RADIO_ACTIVE = pc.green('●'); +const S_RADIO_INACTIVE = pc.dim('○'); +const S_CHECKBOX_LOCKED = pc.green('✓'); +const S_BULLET = pc.green('•'); +const S_BAR = pc.dim('│'); +const S_BAR_H = pc.dim('─'); + +export const cancelSymbol = Symbol('cancel'); + +/** + * Approximate terminal display width (cells) for a string with no ANSI sequences. + * Matches common East Asian / emoji double-width behavior used by modern terminals. + */ +export function approxStringWidth(plain: string): number { + let width = 0; + for (const ch of plain) { + const code = ch.codePointAt(0)!; + if (code === 0) continue; + const wide = + (code >= 0x1100 && code <= 0x115f) || + (code >= 0x231a && code <= 0x231b) || + (code >= 0x2329 && code <= 0x232a) || + (code >= 0x23e9 && code <= 0x23ec) || + code === 0x23f0 || + code === 0x23f3 || + (code >= 0x25fd && code <= 0x25fe) || + (code >= 0x2614 && code <= 0x2615) || + (code >= 0x2648 && code <= 0x2653) || + (code >= 0x267f && code <= 0x267f) || + (code >= 0x2693 && code <= 0x2693) || + (code >= 0x26a1 && code <= 0x26a1) || + (code >= 0x26aa && code <= 0x26ab) || + (code >= 0x26bd && code <= 0x26be) || + (code >= 0x26c4 && code <= 0x26c5) || + (code >= 0x26ce && code <= 0x26ce) || + (code >= 0x26d4 && code <= 0x26d4) || + (code >= 0x26ea && code <= 0x26ea) || + (code >= 0x26f2 && code <= 0x26f3) || + (code >= 0x26f5 && code <= 0x26f5) || + (code >= 0x26fa && code <= 0x26fa) || + (code >= 0x26fd && code <= 0x26fd) || + (code >= 0x2705 && code <= 0x2705) || + (code >= 0x270a && code <= 0x270b) || + (code >= 0x2728 && code <= 0x2728) || + (code >= 0x274c && code <= 0x274c) || + (code >= 0x274e && code <= 0x274e) || + (code >= 0x2753 && code <= 0x2755) || + (code >= 0x2757 && code <= 0x2757) || + (code >= 0x2795 && code <= 0x2797) || + (code >= 0x27b0 && code <= 0x27b0) || + (code >= 0x27bf && code <= 0x27bf) || + (code >= 0x2b1b && code <= 0x2b1c) || + (code >= 0x2b50 && code <= 0x2b50) || + (code >= 0x2b55 && code <= 0x2b55) || + (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || + (code >= 0xa960 && code <= 0xa97c) || + (code >= 0xac00 && code <= 0xd7a3) || + (code >= 0xf900 && code <= 0xfaff) || + (code >= 0xfe10 && code <= 0xfe19) || + (code >= 0xfe30 && code <= 0xfe6f) || + (code >= 0xff00 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + (code >= 0x1f000 && code <= 0x1f9ff); + width += wide ? 2 : 1; + } + return width; +} + +/** + * How many physical terminal rows one logical line occupies after soft-wrapping. + */ +export function visualRowsForLine(line: string, columns: number): number { + const plain = stripVTControlCharacters(line); + const cols = Math.max(1, columns); + const w = approxStringWidth(plain); + return Math.max(1, Math.ceil(w / cols)); +} + +/** + * Total physical rows for a block of logical lines (used to erase/redraw TUI output). + */ +export function countVisualRowsForLines(lines: string[], columns: number | undefined): number { + const cols = + columns !== undefined && columns > 0 + ? columns + : process.stdout.columns && process.stdout.columns > 0 + ? process.stdout.columns + : 80; + return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0); +} + +/** + * Interactive search multiselect prompt. + * Allows users to filter a long list by typing and select multiple items. + * Optionally supports a "locked" section that displays always-selected items. + */ +export async function searchMultiselect( + options: SearchMultiselectOptions +): Promise { + const { + message, + items, + maxVisible = 8, + initialSelected = [], + required = false, + lockedSection, + } = options; + + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: silentOutput, + terminal: false, + }); + + // Enable raw mode for keypress detection + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); + } + readline.emitKeypressEvents(process.stdin, rl); + + let query = ''; + let cursor = 0; + const selected = new Set(initialSelected); + let lastRenderHeight = 0; + + // Locked items are always included in the result + const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : []; + + const filter = (item: SearchItem, q: string): boolean => { + if (!q) return true; + const lowerQ = q.toLowerCase(); + return ( + item.label.toLowerCase().includes(lowerQ) || + String(item.value).toLowerCase().includes(lowerQ) + ); + }; + + const getFiltered = (): SearchItem[] => { + return items.filter((item) => filter(item, query)); + }; + + const clearRender = (): void => { + if (lastRenderHeight > 0) { + // Move up and clear each line + process.stdout.write(`\x1b[${lastRenderHeight}A`); + for (let i = 0; i < lastRenderHeight; i++) { + process.stdout.write('\x1b[2K\x1b[1B'); + } + process.stdout.write(`\x1b[${lastRenderHeight}A`); + } + }; + + const render = (state: 'active' | 'submit' | 'cancel' = 'active'): void => { + clearRender(); + + const lines: string[] = []; + const filtered = getFiltered(); + + // Header + const icon = + state === 'active' ? S_STEP_ACTIVE : state === 'cancel' ? S_STEP_CANCEL : S_STEP_SUBMIT; + lines.push(`${icon} ${pc.bold(message)}`); + + if (state === 'active') { + // Locked section (universal agents) + if (lockedSection && lockedSection.items.length > 0) { + lines.push(`${S_BAR}`); + const lockedTitle = `${pc.bold(lockedSection.title)} ${pc.dim('── always included')}`; + lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`); + for (const item of lockedSection.items) { + lines.push(`${S_BAR} ${S_BULLET} ${pc.bold(item.label)}`); + } + lines.push(`${S_BAR}`); + lines.push( + `${S_BAR} ${S_BAR_H}${S_BAR_H} ${pc.bold('Additional agents')} ${S_BAR_H.repeat(29)}` + ); + } + + // Search input + const searchLine = `${S_BAR} ${pc.dim('Search:')} ${query}${pc.inverse(' ')}`; + lines.push(searchLine); + + // Hint + lines.push(`${S_BAR} ${pc.dim('↑↓ move, space select, enter confirm')}`); + lines.push(`${S_BAR}`); + + // Items + const visibleStart = Math.max( + 0, + Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible) + ); + const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible); + const visibleItems = filtered.slice(visibleStart, visibleEnd); + + if (filtered.length === 0) { + lines.push(`${S_BAR} ${pc.dim('No matches found')}`); + } else { + for (let i = 0; i < visibleItems.length; i++) { + const item = visibleItems[i]!; + const actualIndex = visibleStart + i; + const isSelected = selected.has(item.value); + const isCursor = actualIndex === cursor; + + const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE; + const label = isCursor ? pc.underline(item.label) : item.label; + const hint = item.hint ? pc.dim(` (${item.hint})`) : ''; + + const prefix = isCursor ? pc.cyan('❯') : ' '; + lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`); + } + + // Show count if more items + const hiddenBefore = visibleStart; + const hiddenAfter = filtered.length - visibleEnd; + if (hiddenBefore > 0 || hiddenAfter > 0) { + const parts: string[] = []; + if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`); + if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`); + lines.push(`${S_BAR} ${pc.dim(parts.join(' '))}`); + } + } + + // Selected summary (include locked items) + lines.push(`${S_BAR}`); + const allSelectedLabels = [ + ...(lockedSection ? lockedSection.items.map((i) => i.label) : []), + ...items.filter((item) => selected.has(item.value)).map((item) => item.label), + ]; + if (allSelectedLabels.length === 0) { + lines.push(`${S_BAR} ${pc.dim('Selected: (none)')}`); + } else { + const summary = + allSelectedLabels.length <= 3 + ? allSelectedLabels.join(', ') + : `${allSelectedLabels.slice(0, 3).join(', ')} +${allSelectedLabels.length - 3} more`; + lines.push(`${S_BAR} ${pc.green('Selected:')} ${summary}`); + } + + lines.push(`${pc.dim('└')}`); + } else if (state === 'submit') { + // Final state - show what was selected (including locked) + const allSelectedLabels = [ + ...(lockedSection ? lockedSection.items.map((i) => i.label) : []), + ...items.filter((item) => selected.has(item.value)).map((item) => item.label), + ]; + lines.push(`${S_BAR} ${pc.dim(allSelectedLabels.join(', '))}`); + } else if (state === 'cancel') { + lines.push(`${S_BAR} ${pc.strikethrough(pc.dim('Cancelled'))}`); + } + + process.stdout.write(lines.join('\n') + '\n'); + // Use wrapped row count: logical lines can span multiple terminal rows when hints + // or labels exceed column width. Using lines.length alone under-counts and breaks + // clearRender(), causing the prompt to re-print hundreds of times on each redraw. + lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns); + }; + + const cleanup = (): void => { + process.stdin.removeListener('keypress', keypressHandler); + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + rl.close(); + }; + + const submit = (): void => { + // If required and no locked items, don't allow submitting with no selection + if (required && selected.size === 0 && lockedValues.length === 0) { + return; + } + render('submit'); + cleanup(); + // Include locked values in the result + resolve([...lockedValues, ...Array.from(selected)]); + }; + + const cancel = (): void => { + render('cancel'); + cleanup(); + resolve(cancelSymbol); + }; + + // Handle keypresses + const keypressHandler = (_str: string, key: readline.Key): void => { + if (!key) return; + + const filtered = getFiltered(); + + if (key.name === 'return') { + submit(); + return; + } + + if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { + cancel(); + return; + } + + if (key.name === 'up') { + cursor = Math.max(0, cursor - 1); + render(); + return; + } + + if (key.name === 'down') { + cursor = Math.min(filtered.length - 1, cursor + 1); + render(); + return; + } + + if (key.name === 'space') { + const item = filtered[cursor]; + if (item) { + if (selected.has(item.value)) { + selected.delete(item.value); + } else { + selected.add(item.value); + } + } + render(); + return; + } + + if (key.name === 'backspace') { + query = query.slice(0, -1); + cursor = 0; + render(); + return; + } + + // Regular character input + if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { + query += key.sequence; + cursor = 0; + render(); + return; + } + }; + + process.stdin.on('keypress', keypressHandler); + + // Initial render + render(); + }); +} diff --git a/dt-skill/src/cli/registry.test.ts b/dt-skill/src/cli/registry.test.ts new file mode 100644 index 00000000..10694c69 --- /dev/null +++ b/dt-skill/src/cli/registry.test.ts @@ -0,0 +1,93 @@ +/* @vitest-environment node */ + +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { GlobalOpts } from "./types"; + +const readGlobalConfig = vi.fn(); +const writeGlobalConfig = vi.fn(); +const discoverRegistryFromSite = vi.fn(); + +vi.mock("../config.js", () => ({ + readGlobalConfig: (...args: unknown[]) => readGlobalConfig(...args), + writeGlobalConfig: (...args: unknown[]) => writeGlobalConfig(...args), +})); + +vi.mock("../discovery.js", () => ({ + discoverRegistryFromSite: (...args: unknown[]) => discoverRegistryFromSite(...args), +})); + +const { DEFAULT_REGISTRY, DEFAULT_SITE, getRegistry, resolveRegistry } = await import("./registry"); + +function makeOpts(overrides: Partial = {}): GlobalOpts { + return { + workdir: "/work", + dir: "/work/skills", + site: "", + registry: DEFAULT_REGISTRY, + registrySource: "default", + ...overrides, + }; +} + +beforeEach(() => { + readGlobalConfig.mockReset(); + writeGlobalConfig.mockReset(); + discoverRegistryFromSite.mockReset(); +}); + +describe("registry resolution", () => { + it("has no static site or registry fallback", () => { + expect(DEFAULT_SITE).toBe(""); + expect(DEFAULT_REGISTRY).toBe(""); + }); + + it("prefers explicit registry over discovery/cache", async () => { + readGlobalConfig.mockResolvedValue({ registry: "https://auth.clawdhub.com" }); + discoverRegistryFromSite.mockResolvedValue({ apiBase: "https://clawhub.ai" }); + + const registry = await resolveRegistry( + makeOpts({ registry: "https://custom.example", registrySource: "cli" }), + ); + + expect(registry).toBe("https://custom.example"); + expect(discoverRegistryFromSite).not.toHaveBeenCalled(); + }); + + it("ignores legacy registry and updates cache from discovery", async () => { + readGlobalConfig.mockResolvedValue({ registry: "https://auth.clawdhub.com", token: "tkn" }); + discoverRegistryFromSite.mockResolvedValue({ apiBase: "http://10.0.0.8:7001" }); + + const registry = await getRegistry(makeOpts({ site: "http://10.0.0.8:7001" }), { cache: true }); + + expect(registry).toBe("http://10.0.0.8:7001"); + expect(writeGlobalConfig).toHaveBeenCalledWith({ + registry: "http://10.0.0.8:7001", + token: "tkn", + }); + }); + + it("fails clearly when no explicit, cached, or discoverable registry exists", async () => { + readGlobalConfig.mockResolvedValue({ registry: "https://registry.clawhub.ai", token: "tkn" }); + discoverRegistryFromSite.mockResolvedValue(null); + + await expect(getRegistry(makeOpts(), { cache: true })).rejects.toThrow( + "Registry is not configured", + ); + expect(writeGlobalConfig).not.toHaveBeenCalled(); + }); + + it("caches an explicit runtime registry even when another custom registry was cached", async () => { + readGlobalConfig.mockResolvedValue({ registry: "http://10.0.0.7:7001" }); + + const registry = await getRegistry( + makeOpts({ registry: "http://10.0.0.8:7001", registrySource: "cli" }), + { cache: true }, + ); + + expect(registry).toBe("http://10.0.0.8:7001"); + expect(writeGlobalConfig).toHaveBeenCalledWith({ + registry: "http://10.0.0.8:7001", + token: undefined, + }); + }); +}); diff --git a/dt-skill/src/cli/registry.ts b/dt-skill/src/cli/registry.ts new file mode 100644 index 00000000..4341ee71 --- /dev/null +++ b/dt-skill/src/cli/registry.ts @@ -0,0 +1,54 @@ +import { readGlobalConfig, writeGlobalConfig } from "../config.js"; +import { discoverRegistryFromSite } from "../discovery.js"; +import type { GlobalOpts } from "./types.js"; + +export const DEFAULT_SITE = ""; +export const DEFAULT_REGISTRY = ""; +const LEGACY_REGISTRY_HOSTS = new Set([ + "auth.clawdhub.com", + "auth.clawhub.com", + "auth.clawhub.ai", + "registry.clawhub.ai", +]); + +export async function resolveRegistry(opts: GlobalOpts) { + const explicit = opts.registrySource !== "default" ? opts.registry.trim() : ""; + if (explicit) return explicit; + + const cfg = await readGlobalConfig(); + const cached = cfg?.registry?.trim(); + if (cached && !isLegacyRegistry(cached)) return cached; + + const site = opts.site.trim(); + if (site) { + const discovery = await discoverRegistryFromSite(site).catch(() => null); + const discovered = discovery?.apiBase?.trim(); + if (discovered) return discovered; + } + + throw new Error( + "Registry is not configured. Copy a command from the Doraemon Skills page or pass --registry .", + ); +} + +export async function getRegistry(opts: GlobalOpts, params?: { cache?: boolean }) { + const cache = params?.cache !== false; + const registry = await resolveRegistry(opts); + if (!cache) return registry; + const cfg = await readGlobalConfig(); + const cached = cfg?.registry?.trim(); + const shouldUpdate = + !cached || + isLegacyRegistry(cached) || + cached !== registry; + if (shouldUpdate) await writeGlobalConfig({ registry, token: cfg?.token }); + return registry; +} + +function isLegacyRegistry(registry: string) { + try { + return LEGACY_REGISTRY_HOSTS.has(new URL(registry).hostname); + } catch { + return false; + } +} diff --git a/dt-skill/src/cli/scanSkills.test.ts b/dt-skill/src/cli/scanSkills.test.ts new file mode 100644 index 00000000..40a31e24 --- /dev/null +++ b/dt-skill/src/cli/scanSkills.test.ts @@ -0,0 +1,66 @@ +/* @vitest-environment node */ + +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { findSkillFolders, getFallbackSkillRoots } from "./scanSkills"; + +async function makeTmpDir() { + return mkdtemp(join(tmpdir(), "clawhub-scan-")); +} + +describe("scanSkills", () => { + it("detects a single skill folder (root contains SKILL.md)", async () => { + const root = await makeTmpDir(); + try { + await writeFile(join(root, "SKILL.md"), "# Skill\n", "utf8"); + const found = await findSkillFolders(root); + expect(found).toHaveLength(1); + expect(found[0]?.folder).toBe(resolve(root)); + expect(found[0]?.slug).toBeTruthy(); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("detects skills in a skills directory (subfolders)", async () => { + const root = await makeTmpDir(); + try { + const skillsDir = join(root, "skills"); + const folder = join(skillsDir, "cool-skill"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + + const found = await findSkillFolders(skillsDir); + expect(found).toHaveLength(1); + expect(found[0]?.slug).toBe("cool-skill"); + expect(found[0]?.folder).toBe(resolve(folder)); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("ignores plural skills.md marker files", async () => { + const root = await makeTmpDir(); + try { + const folder = join(root, "docs"); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, "skills.md"), "# Docs\n", "utf8"); + + const found = await findSkillFolders(root); + expect(found).toHaveLength(0); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("includes known legacy roots", () => { + const roots = getFallbackSkillRoots("/tmp/anywhere"); + expect(roots.some((p) => p.endsWith("/clawdis/skills"))).toBe(true); + expect(roots.some((p) => p.endsWith("/clawd/skills"))).toBe(true); + expect(roots.some((p) => p.endsWith("/clawdbot/skills"))).toBe(true); + expect(roots.some((p) => p.endsWith("/openclaw/skills"))).toBe(true); + expect(roots.some((p) => p.endsWith("/moltbot/skills"))).toBe(true); + }); +}); diff --git a/dt-skill/src/cli/scanSkills.ts b/dt-skill/src/cli/scanSkills.ts new file mode 100644 index 00000000..3bbf1c14 --- /dev/null +++ b/dt-skill/src/cli/scanSkills.ts @@ -0,0 +1,102 @@ +import { readdir, stat } from "node:fs/promises"; +import { basename, join, resolve } from "node:path"; +import { resolveHome } from "../homedir.js"; +import { sanitizeSlug, titleCase } from "./slug.js"; + +export type SkillFolder = { + folder: string; + slug: string; + displayName: string; +}; + +export async function findSkillFolders(root: string): Promise { + const absRoot = resolve(root); + const rootStat = await stat(absRoot).catch(() => null); + if (!rootStat || !rootStat.isDirectory()) return []; + + const direct = await isSkillFolder(absRoot); + if (direct) return [direct]; + + const entries = await readdir(absRoot, { withFileTypes: true }).catch(() => []); + const folders = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => join(absRoot, entry.name)); + const results: SkillFolder[] = []; + for (const folder of folders) { + const found = await isSkillFolder(folder); + if (found) results.push(found); + } + return results.sort((a, b) => a.slug.localeCompare(b.slug)); +} + +export function getFallbackSkillRoots(workdir: string) { + const home = resolveHome(); + const roots = [ + // adjacent repo installs + resolve(workdir, "..", "clawdis", "skills"), + resolve(workdir, "..", "clawdis", "Skills"), + resolve(workdir, "..", "clawdbot", "skills"), + resolve(workdir, "..", "clawdbot", "Skills"), + resolve(workdir, "..", "openclaw", "skills"), + resolve(workdir, "..", "openclaw", "Skills"), + resolve(workdir, "..", "moltbot", "skills"), + resolve(workdir, "..", "moltbot", "Skills"), + + // legacy locations + resolve(home, "clawd", "skills"), + resolve(home, "clawd", "Skills"), + resolve(home, ".clawd", "skills"), + resolve(home, ".clawd", "Skills"), + + resolve(home, "clawdbot", "skills"), + resolve(home, "clawdbot", "Skills"), + resolve(home, ".clawdbot", "skills"), + resolve(home, ".clawdbot", "Skills"), + + resolve(home, "clawdis", "skills"), + resolve(home, "clawdis", "Skills"), + resolve(home, ".clawdis", "skills"), + resolve(home, ".clawdis", "Skills"), + + resolve(home, "openclaw", "skills"), + resolve(home, "openclaw", "Skills"), + resolve(home, ".openclaw", "skills"), + resolve(home, ".openclaw", "Skills"), + + resolve(home, "moltbot", "skills"), + resolve(home, "moltbot", "Skills"), + resolve(home, ".moltbot", "skills"), + resolve(home, ".moltbot", "Skills"), + + // macOS App Support legacy + resolve(home, "Library", "Application Support", "clawdbot", "skills"), + resolve(home, "Library", "Application Support", "clawdbot", "Skills"), + resolve(home, "Library", "Application Support", "clawdis", "skills"), + resolve(home, "Library", "Application Support", "clawdis", "Skills"), + resolve(home, "Library", "Application Support", "openclaw", "skills"), + resolve(home, "Library", "Application Support", "openclaw", "Skills"), + resolve(home, "Library", "Application Support", "moltbot", "skills"), + resolve(home, "Library", "Application Support", "moltbot", "Skills"), + ]; + return Array.from(new Set(roots)); +} + +async function isSkillFolder(folder: string): Promise { + const marker = await findSkillMarker(folder); + if (!marker) return null; + const base = basename(folder); + const slug = sanitizeSlug(base); + if (!slug) return null; + const displayName = titleCase(base); + return { folder, slug, displayName }; +} + +async function findSkillMarker(folder: string) { + const candidates = ["SKILL.md", "skill.md"]; + for (const name of candidates) { + const path = join(folder, name); + const st = await stat(path).catch(() => null); + if (st?.isFile()) return path; + } + return null; +} diff --git a/dt-skill/src/cli/slug.ts b/dt-skill/src/cli/slug.ts new file mode 100644 index 00000000..ae7d8f40 --- /dev/null +++ b/dt-skill/src/cli/slug.ts @@ -0,0 +1,16 @@ +export function sanitizeSlug(value: string) { + const raw = value + .trim() + .toLowerCase() + .replace(/[^a-z0-9-]+/g, "-"); + const cleaned = raw.replace(/^-+/, "").replace(/-+$/, "").replace(/--+/g, "-"); + return cleaned; +} + +export function titleCase(value: string) { + return value + .trim() + .replace(/[-_]+/g, " ") + .replace(/\s+/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); +} diff --git a/dt-skill/src/cli/types.ts b/dt-skill/src/cli/types.ts new file mode 100644 index 00000000..c49a8f19 --- /dev/null +++ b/dt-skill/src/cli/types.ts @@ -0,0 +1,15 @@ +export type GlobalOpts = { + workdir: string; + dir: string; + site: string; + registry: string; + registrySource: "cli" | "env" | "default"; + agent?: string; + globalScope?: boolean; + globalScopeExplicit?: boolean; +}; + +export type ResolveResult = { + match: { version: string } | null; + latestVersion: { version: string } | null; +}; diff --git a/dt-skill/src/cli/ui.test.ts b/dt-skill/src/cli/ui.test.ts new file mode 100644 index 00000000..6676c64f --- /dev/null +++ b/dt-skill/src/cli/ui.test.ts @@ -0,0 +1,77 @@ +/* @vitest-environment node */ + +import { describe, expect, it, vi } from "vitest"; + +const mockSpawn = vi.fn(); +const originalPlatform = process.platform; + +vi.mock("node:child_process", () => ({ + spawn: (...args: unknown[]) => mockSpawn(...args), +})); + +const { openInBrowser } = await import("./ui"); + +type ErrorHandler = (error: NodeJS.ErrnoException) => void; + +function createMockChild() { + let onError: ErrorHandler | null = null; + const child = { + on: vi.fn((event: string, handler: ErrorHandler) => { + if (event === "error") onError = handler; + return child; + }), + unref: vi.fn(), + emitError: (error: NodeJS.ErrnoException) => onError?.(error), + }; + return child; +} + +describe("openInBrowser", () => { + it("uses explorer on Windows and preserves query params in the URL argument", () => { + const child = createMockChild(); + mockSpawn.mockReturnValueOnce(child); + const url = + "https://clawhub.ai/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2Fcallback&state=abc123"; + + try { + Object.defineProperty(process, "platform", { value: "win32" }); + openInBrowser(url); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform }); + } + + expect(mockSpawn).toHaveBeenCalledWith("explorer", [url], { + stdio: "ignore", + detached: true, + }); + expect(child.unref).toHaveBeenCalledOnce(); + }); + + it("prints manual URL instructions when browser opener is missing", () => { + const child = createMockChild(); + mockSpawn.mockReturnValueOnce(child); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + openInBrowser("https://clawhub.ai"); + child.emitError(Object.assign(new Error("not found"), { code: "ENOENT" })); + + expect(logSpy).toHaveBeenCalledWith("Could not open browser automatically."); + expect(logSpy).toHaveBeenCalledWith("Please open this URL manually:"); + expect(logSpy).toHaveBeenCalledWith(" https://clawhub.ai"); + expect(child.unref).toHaveBeenCalledOnce(); + logSpy.mockRestore(); + }); + + it("does not print manual instructions for non-ENOENT errors", () => { + const child = createMockChild(); + mockSpawn.mockReturnValueOnce(child); + const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + openInBrowser("https://clawhub.ai"); + child.emitError(Object.assign(new Error("permission denied"), { code: "EACCES" })); + + expect(logSpy).not.toHaveBeenCalledWith("Could not open browser automatically."); + expect(child.unref).toHaveBeenCalledOnce(); + logSpy.mockRestore(); + }); +}); diff --git a/dt-skill/src/cli/ui.ts b/dt-skill/src/cli/ui.ts new file mode 100644 index 00000000..12998c7b --- /dev/null +++ b/dt-skill/src/cli/ui.ts @@ -0,0 +1,137 @@ +import { spawn } from "node:child_process"; +import { stdin } from "node:process"; +import { confirm, isCancel, select } from "@clack/prompts"; +import ora from "ora"; +import { listAgentNames, getAgentLabel, resolveAgentWorkdir, AGENTS } from "./agents.js"; +import type { AgentName } from "./agents.js"; + +export async function promptHidden(prompt: string) { + if (!stdin.isTTY) return ""; + process.stdout.write(prompt); + const chunks: Buffer[] = []; + stdin.setRawMode(true); + stdin.resume(); + return new Promise((resolvePromise) => { + function onData(data: Buffer) { + const text = data.toString("utf8"); + if (text === "\r" || text === "\n") { + stdin.setRawMode(false); + stdin.pause(); + stdin.off("data", onData); + process.stdout.write("\n"); + resolvePromise(Buffer.concat(chunks).toString("utf8").trim()); + return; + } + if (text === "\u0003") { + stdin.setRawMode(false); + stdin.pause(); + stdin.off("data", onData); + process.stdout.write("\n"); + fail("Canceled"); + } + if (text === "\u007f") { + chunks.pop(); + return; + } + chunks.push(data); + } + stdin.on("data", onData); + }); +} + +export async function promptConfirm(prompt: string) { + const answer = await confirm({ message: prompt }); + if (isCancel(answer)) return false; + return answer; +} + +export function openInBrowser(url: string) { + const args = + process.platform === "darwin" + ? ["open", url] + : process.platform === "win32" + ? ["explorer", url] + : ["xdg-open", url]; + const [command, ...commandArgs] = args; + if (!command) return; + + const child = spawn(command, commandArgs, { stdio: "ignore", detached: true }); + + child.on("error", (err) => { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + console.log(""); + console.log("Could not open browser automatically."); + console.log("Please open this URL manually:"); + console.log(""); + console.log(` ${url}`); + console.log(""); + } + }); + + child.unref(); +} + +export function isInteractive() { + return process.stdout.isTTY && stdin.isTTY; +} + +export function createSpinner(text: string) { + return ora({ text, spinner: "dots", isEnabled: isInteractive() }).start(); +} + +export function formatError(error: unknown) { + if (error instanceof Error) return error.message; + return String(error); +} + +export function fail(message: string): never { + throw new Error(message); +} + +export async function selectAgent(): Promise<{ agent: AgentName; workdir: string; dir: string } | null> { + if (!isInteractive()) return null; + + const names = listAgentNames(); + const options = names.map((name) => ({ + value: name, + label: getAgentLabel(name), + })); + + const selected = await select({ + message: "Select target agent:", + options, + }); + + if (isCancel(selected)) return null; + const agent = selected as AgentName; + const workdir = resolveAgentWorkdir(agent, false); + const dir = `${workdir}/skills`; + return { agent, workdir, dir }; +} + +export async function selectScope(agent: AgentName): Promise { + if (!isInteractive()) return null; + + // Check if the selected agent supports global installation + const supportsGlobal = AGENTS[agent].globalWorkdir !== undefined; + if (!supportsGlobal) return false; + + const scope = await select({ + message: "安装范围", + options: [ + { + value: false, + label: "Project", + hint: "在当前目录安装(随项目提交)", + }, + { + value: true, + label: "Global", + hint: "在 home 目录安装(跨项目可用)", + }, + ], + }); + + if (isCancel(scope)) return null; + return scope as boolean; +} diff --git a/dt-skill/src/config.test.ts b/dt-skill/src/config.test.ts new file mode 100644 index 00000000..6eeb1992 --- /dev/null +++ b/dt-skill/src/config.test.ts @@ -0,0 +1,87 @@ +/* @vitest-environment node */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createEnvStubRegistry } from "../test/runtimeStubs.js"; + +const fsMocks = vi.hoisted(() => ({ + chmod: vi.fn(), + mkdir: vi.fn(), + readFile: vi.fn(), + writeFile: vi.fn(), +})); + +vi.mock("node:fs/promises", async () => { + const actual = await vi.importActual("node:fs/promises"); + return { + ...actual, + chmod: fsMocks.chmod, + mkdir: fsMocks.mkdir, + readFile: fsMocks.readFile, + writeFile: fsMocks.writeFile, + }; +}); + +const configModuleSpecifier = "./config.js?config-test" as string; + +const { writeGlobalConfig } = (await import(configModuleSpecifier)) as typeof import("./config"); + +const originalPlatform = process.platform; +const testConfigPath = "/tmp/clawhub-config-test/config.json"; +const envStubs = createEnvStubRegistry(); + +function makeErr(code: string): NodeJS.ErrnoException { + const error = new Error(code) as NodeJS.ErrnoException; + error.code = code; + return error; +} + +beforeEach(() => { + envStubs.stub("CLAWHUB_CONFIG_PATH", testConfigPath); + Object.defineProperty(process, "platform", { value: "linux" }); + fsMocks.chmod.mockResolvedValue(undefined); + fsMocks.mkdir.mockResolvedValue(undefined); + fsMocks.readFile.mockResolvedValue(""); + fsMocks.writeFile.mockResolvedValue(undefined); +}); + +afterEach(() => { + Object.defineProperty(process, "platform", { value: originalPlatform }); + envStubs.restoreAll(); + vi.clearAllMocks(); + fsMocks.chmod.mockReset(); + fsMocks.mkdir.mockReset(); + fsMocks.readFile.mockReset(); + fsMocks.writeFile.mockReset(); +}); + +describe("writeGlobalConfig", () => { + it("writes config with restricted modes", async () => { + await writeGlobalConfig({ registry: "https://example.com", token: "clh_test" }); + + expect(fsMocks.mkdir).toHaveBeenCalledWith("/tmp/clawhub-config-test", { + recursive: true, + mode: 0o700, + }); + expect(fsMocks.writeFile).toHaveBeenCalledWith( + testConfigPath, + expect.stringContaining('"token": "clh_test"'), + { + encoding: "utf8", + mode: 0o600, + }, + ); + expect(fsMocks.chmod).toHaveBeenCalledWith(testConfigPath, 0o600); + }); + + it("ignores non-fatal chmod errors", async () => { + fsMocks.chmod.mockRejectedValueOnce(makeErr("ENOTSUP")); + + await expect(writeGlobalConfig({ registry: "https://example.com" })).resolves.toBeUndefined(); + }); + + it("rethrows unexpected chmod errors", async () => { + fsMocks.chmod.mockRejectedValueOnce(new Error("boom")); + + await expect(writeGlobalConfig({ registry: "https://example.com" })).rejects.toThrow("boom"); + }); +}); diff --git a/dt-skill/src/config.ts b/dt-skill/src/config.ts new file mode 100644 index 00000000..1bc5264a --- /dev/null +++ b/dt-skill/src/config.ts @@ -0,0 +1,83 @@ +import { existsSync } from "node:fs"; +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { resolveHome } from "./homedir.js"; +import { type GlobalConfig, GlobalConfigSchema, parseArk } from "./schema/index.js"; + +/** + * Resolve config path with legacy fallback. + * Checks for 'clawhub' first, falls back to legacy 'clawdhub' if it exists. + */ +function resolveConfigPath(baseDir: string): string { + const clawhubPath = join(baseDir, "clawhub", "config.json"); + const clawdhubPath = join(baseDir, "clawdhub", "config.json"); + if (existsSync(clawhubPath)) return clawhubPath; + if (existsSync(clawdhubPath)) return clawdhubPath; + return clawhubPath; +} + +function isNonFatalChmodError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + const code = (error as NodeJS.ErrnoException).code; + return code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EINVAL"; +} + +function getGlobalConfigPath() { + const override = + process.env.CLAWHUB_CONFIG_PATH?.trim() ?? process.env.CLAWDHUB_CONFIG_PATH?.trim(); + if (override) return resolve(override); + + const home = resolveHome(); + + if (process.platform === "darwin") { + return resolveConfigPath(join(home, "Library", "Application Support")); + } + + const xdg = process.env.XDG_CONFIG_HOME; + if (xdg) { + return resolveConfigPath(xdg); + } + + if (process.platform === "win32") { + const appData = process.env.APPDATA; + if (appData) { + return resolveConfigPath(appData); + } + } + + return resolveConfigPath(join(home, ".config")); +} + +export async function readGlobalConfig(): Promise { + try { + const raw = await readFile(getGlobalConfigPath(), "utf8"); + const parsed = JSON.parse(raw) as unknown; + return parseArk(GlobalConfigSchema, parsed, "Global config"); + } catch { + return null; + } +} + +export async function writeGlobalConfig(config: GlobalConfig) { + const path = getGlobalConfigPath(); + const dir = dirname(path); + + // Create directory with restricted permissions (owner only) + await mkdir(dir, { recursive: true, mode: 0o700 }); + + // Write file with restricted permissions (owner read/write only) + // This protects API tokens from being read by other users + await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { + encoding: "utf8", + mode: 0o600, + }); + + // Ensure permissions on existing files (writeFile mode only applies on create) + if (process.platform !== "win32") { + try { + await chmod(path, 0o600); + } catch (error) { + if (!isNonFatalChmodError(error)) throw error; + } + } +} diff --git a/dt-skill/src/deviceAuth.test.ts b/dt-skill/src/deviceAuth.test.ts new file mode 100644 index 00000000..ff3f5a51 --- /dev/null +++ b/dt-skill/src/deviceAuth.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it, vi } from "vitest"; +import { pollForDeviceToken, requestDeviceCode } from "./deviceAuth.js"; + +describe("deviceAuth", () => { + describe("requestDeviceCode", () => { + it("should POST to /api/cli/device/code and return device code response", async () => { + const mockResponse = { + device_code: "abc123", + user_code: "ABCD-1234", + verification_uri: "https://clawhub.ai/device", + expires_in: 900, + interval: 5, + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockResponse), + }); + + const result = await requestDeviceCode({ + apiUrl: "https://api.example", + siteUrl: "https://clawhub.ai", + }); + + expect(result).toEqual(mockResponse); + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example/api/cli/device/code", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json", + }), + }), + ); + }); + + it("should throw on non-ok response", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + text: () => Promise.resolve("endpoint not found"), + }); + + await expect( + requestDeviceCode({ apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }), + ).rejects.toThrow("Device code request failed (404)"); + }); + + it("should throw on invalid response (missing fields)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ device_code: "abc" }), + }); + + await expect( + requestDeviceCode({ apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }), + ).rejects.toThrow("Invalid device code response"); + }); + }); + + describe("pollForDeviceToken", () => { + it("should return token on successful authorization", async () => { + const tokenResponse = { + access_token: "token123", + token_type: "bearer", + scope: "read write", + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(tokenResponse), + }); + + const result = await pollForDeviceToken( + { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, + "device_code_123", + { interval: 0.01, expiresIn: 10 }, + ); + + expect(result.access_token).toBe("token123"); + }); + + it("should keep polling on authorization_pending", async () => { + let callCount = 0; + global.fetch = vi.fn().mockImplementation(() => { + callCount++; + if (callCount < 3) { + return Promise.resolve({ + ok: false, + json: () => Promise.resolve({ error: "authorization_pending" }), + }); + } + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + access_token: "token_after_wait", + token_type: "bearer", + scope: "read write", + }), + }); + }); + + const result = await pollForDeviceToken( + { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, + "device_code_123", + { interval: 0.01, expiresIn: 10 }, + ); + + expect(result.access_token).toBe("token_after_wait"); + expect(callCount).toBe(3); + }); + + it("should throw on access_denied", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: "access_denied" }), + }); + + await expect( + pollForDeviceToken( + { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, + "device_code_123", + { + interval: 0.01, + expiresIn: 10, + }, + ), + ).rejects.toThrow("Authorization denied"); + }); + + it("should throw on expired_token", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: "expired_token" }), + }); + + await expect( + pollForDeviceToken( + { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, + "device_code_123", + { + interval: 0.01, + expiresIn: 10, + }, + ), + ).rejects.toThrow("expired"); + }); + }); +}); diff --git a/dt-skill/src/deviceAuth.ts b/dt-skill/src/deviceAuth.ts new file mode 100644 index 00000000..207f2a17 --- /dev/null +++ b/dt-skill/src/deviceAuth.ts @@ -0,0 +1,151 @@ +/** + * GitHub Device Flow authentication for headless environments. + * + * Implements RFC 8628 / GitHub's Device Flow: + * https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow + * + * This allows CLI authentication without a browser redirect to localhost, + * enabling headless agents and remote servers to authenticate. + */ + +type DeviceCodeResponse = { + device_code: string; + user_code: string; + verification_uri: string; + expires_in: number; + interval: number; +}; + +type DeviceTokenResponse = { + access_token: string; + token_type: string; + scope: string; +}; + +type DeviceTokenErrorResponse = { + error: string; + error_description?: string; + interval?: number; +}; + +type DeviceFlowConfig = { + /** The ClawHub API URL that exposes device flow endpoints */ + apiUrl: string; + /** The ClawHub site URL that hosts the verification page */ + siteUrl: string; + /** Client ID for the OAuth app (provided by ClawHub) */ + clientId?: string; + /** Scope to request */ + scope?: string; +}; + +const DEFAULT_SCOPE = "read write"; + +/** + * Request a device code from the ClawHub device flow endpoint. + */ +export async function requestDeviceCode(config: DeviceFlowConfig): Promise { + const url = new URL("/api/cli/device/code", config.apiUrl); + + const body: Record = { + scope: config.scope ?? DEFAULT_SCOPE, + site_url: config.siteUrl, + }; + if (config.clientId) { + body.client_id = config.clientId; + } + + const response = await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error( + `Device code request failed (${response.status}): ${text || response.statusText}`, + ); + } + + const data = (await response.json()) as DeviceCodeResponse; + + if (!data.device_code || !data.user_code || !data.verification_uri) { + throw new Error("Invalid device code response from server"); + } + + return data; +} + +/** + * Poll for the device flow token until the user completes authorization, + * the code expires, or an unrecoverable error occurs. + */ +export async function pollForDeviceToken( + config: DeviceFlowConfig, + deviceCode: string, + options: { interval: number; expiresIn: number }, +): Promise { + const url = new URL("/api/cli/device/token", config.apiUrl); + const deadline = Date.now() + options.expiresIn * 1000; + let interval = options.interval * 1000; + + const body: Record = { + device_code: deviceCode, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }; + if (config.clientId) { + body.client_id = config.clientId; + } + + while (Date.now() < deadline) { + await sleep(interval); + + const response = await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(body), + }); + + // Parse JSON once to avoid "body already read" errors + const data = (await response.json().catch(() => ({}))) as + | DeviceTokenResponse + | DeviceTokenErrorResponse; + + if (response.ok && "access_token" in data && data.access_token) { + return data as DeviceTokenResponse; + } + + const errorData = data as DeviceTokenErrorResponse; + + switch (errorData.error) { + case "authorization_pending": + // User hasn't completed auth yet — keep polling + break; + case "slow_down": + // Server requests longer interval + interval = (errorData.interval ?? Math.ceil(interval / 1000) + 5) * 1000; + break; + case "expired_token": + throw new Error("Device code expired. Please try again."); + case "access_denied": + throw new Error("Authorization denied by user."); + default: + throw new Error( + `Device flow error: ${errorData.error_description || errorData.error || "unknown error"}`, + ); + } + } + + throw new Error("Device code expired (timeout). Please try again."); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/dt-skill/src/discovery.test.ts b/dt-skill/src/discovery.test.ts new file mode 100644 index 00000000..4e26b4ac --- /dev/null +++ b/dt-skill/src/discovery.test.ts @@ -0,0 +1,80 @@ +/* @vitest-environment node */ + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createGlobalStubRegistry } from "../test/runtimeStubs.js"; +import { discoverRegistryFromSite } from "./discovery"; + +const globalStubs = createGlobalStubRegistry(); + +describe("discovery", () => { + afterEach(() => { + globalStubs.restoreAll(); + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + it("returns null on non-ok response", async () => { + globalStubs.stub( + "fetch", + vi.fn(async () => new Response("nope", { status: 404 })) as unknown as typeof fetch, + ); + await expect(discoverRegistryFromSite("https://example.com")).resolves.toBeNull(); + }); + + it("parses registry config", async () => { + globalStubs.stub( + "fetch", + vi.fn( + async () => + new Response(JSON.stringify({ registry: "https://example.convex.site" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ) as unknown as typeof fetch, + ); + await expect(discoverRegistryFromSite("https://example.com")).resolves.toEqual({ + apiBase: "https://example.convex.site", + authBase: undefined, + minCliVersion: undefined, + }); + }); + + it("parses apiBase config", async () => { + globalStubs.stub( + "fetch", + vi.fn( + async () => + new Response( + JSON.stringify({ + apiBase: "https://api.example.com", + authBase: "https://auth.example.com", + minCliVersion: "1.2.3", + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ) as unknown as typeof fetch, + ); + await expect(discoverRegistryFromSite("https://example.com")).resolves.toEqual({ + apiBase: "https://api.example.com", + authBase: "https://auth.example.com", + minCliVersion: "1.2.3", + }); + }); + + it("returns null when apiBase is empty", async () => { + globalStubs.stub( + "fetch", + vi.fn( + async () => + new Response(JSON.stringify({ apiBase: "" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ) as unknown as typeof fetch, + ); + await expect(discoverRegistryFromSite("https://example.com")).resolves.toBeNull(); + }); +}); diff --git a/dt-skill/src/discovery.ts b/dt-skill/src/discovery.ts new file mode 100644 index 00000000..1a9964eb --- /dev/null +++ b/dt-skill/src/discovery.ts @@ -0,0 +1,23 @@ +import { parseArk, WellKnownConfigSchema } from "./schema/index.js"; + +export async function discoverRegistryFromSite(siteUrl: string) { + const paths = ["/.well-known/clawhub.json", "/.well-known/clawdhub.json"]; + for (const path of paths) { + const url = new URL(path, siteUrl); + const response = await fetch(url.toString(), { + method: "GET", + headers: { Accept: "application/json" }, + }); + if (!response.ok) continue; + const raw = (await response.json()) as unknown; + const parsed = parseArk(WellKnownConfigSchema, raw, "WellKnown config"); + const apiBase = "apiBase" in parsed ? parsed.apiBase : parsed.registry; + if (!apiBase) return null; + return { + apiBase, + authBase: parsed.authBase, + minCliVersion: parsed.minCliVersion, + }; + } + return null; +} diff --git a/dt-skill/src/homedir.ts b/dt-skill/src/homedir.ts new file mode 100644 index 00000000..92f81e7e --- /dev/null +++ b/dt-skill/src/homedir.ts @@ -0,0 +1,29 @@ +import { homedir } from "node:os"; +import { win32 } from "node:path"; + +/** + * Resolve the user's home directory, preferring environment variables over + * os.homedir(). On Linux, os.homedir() reads from /etc/passwd which can + * return a stale path after a user rename (usermod -l). The $HOME env var + * is set by the login process and reflects the current session. + */ +export function resolveHome(): string { + if (process.platform === "win32") { + return normalizeHome(process.env.USERPROFILE) || normalizeHome(process.env.HOME) || homedir(); + } + return normalizeHome(process.env.HOME) || homedir(); +} + +function normalizeHome(value: string | undefined): string { + const trimmed = value?.trim(); + if (!trimmed) return ""; + + if (process.platform === "win32") { + const root = win32.parse(trimmed).root; + if (trimmed === root) return trimmed; + return trimmed.replace(/[\\/]+$/, ""); + } + + if (trimmed === "/") return "/"; + return trimmed.replace(/\/+$/, ""); +} diff --git a/dt-skill/src/http.bun.test.ts b/dt-skill/src/http.bun.test.ts new file mode 100644 index 00000000..48b15f03 --- /dev/null +++ b/dt-skill/src/http.bun.test.ts @@ -0,0 +1,190 @@ +/* @vitest-environment node */ + +import { describe, expect, it, vi } from "vitest"; +import { createHttpClient } from "./http.js"; + +type SpawnResult = { + status: number | null; + stdout?: string; + stderr?: string; +}; + +function createBunClient(options?: { + spawnImpl?: (...args: unknown[]) => SpawnResult; + mkdtempValue?: string; + readFileValue?: Buffer | null; +}) { + const spawnImpl = vi.fn(options?.spawnImpl ?? (() => ({ status: 0, stdout: "", stderr: "" }))); + const mkdirImpl = vi.fn(async () => undefined); + const mkdtempImpl = vi.fn(async () => options?.mkdtempValue ?? "/tmp/clawhub-test"); + const rmImpl = vi.fn(async () => undefined); + const writeFileImpl = vi.fn(async () => undefined); + const readFileImpl = vi.fn( + async () => (options?.readFileValue ?? Buffer.from([1, 2, 3])) as Buffer, + ); + const setTimeoutImpl = vi.fn((callback: () => void, _ms?: number) => { + callback(); + return 1 as unknown as ReturnType; + }); + const clearTimeoutImpl = vi.fn(); + + return { + client: createHttpClient({ + runtime: "bun", + configureDispatcher: false, + spawnSyncImpl: spawnImpl as unknown as typeof import("node:child_process").spawnSync, + mkdirImpl: mkdirImpl as unknown as typeof import("node:fs/promises").mkdir, + mkdtempImpl: mkdtempImpl as unknown as typeof import("node:fs/promises").mkdtemp, + rmImpl: rmImpl as unknown as typeof import("node:fs/promises").rm, + writeFileImpl: writeFileImpl as unknown as typeof import("node:fs/promises").writeFile, + readFileImpl: readFileImpl as unknown as typeof import("node:fs/promises").readFile, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + tmpdirPath: "/tmp", + random: () => 0, + }), + spawnImpl, + mkdirImpl, + mkdtempImpl, + rmImpl, + writeFileImpl, + readFileImpl, + setTimeoutImpl, + clearTimeoutImpl, + }; +} + +describe("bun http client", () => { + it("uses curl for apiRequest GET and POST", async () => { + const { client, spawnImpl } = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: "" }), + }); + + const getResult = await client.apiRequest<{ ok: boolean }>("https://registry.example", { + method: "GET", + path: "/v1/ping", + token: "clh_token", + }); + await client.apiRequest("https://registry.example", { + method: "POST", + path: "/v1/ping", + body: { a: 1 }, + }); + + expect(getResult).toEqual({ ok: true }); + const [, getArgs] = spawnImpl.mock.calls[0] as [string, string[]]; + expect(getArgs).toContain("GET"); + expect(getArgs).toContain("https://registry.example/v1/ping"); + expect(getArgs).toContain("Authorization: Bearer clh_token"); + + const [, postArgs] = spawnImpl.mock.calls[1] as [string, string[]]; + expect(postArgs).toContain("Content-Type: application/json"); + expect(postArgs).toContain("--data-binary"); + expect(postArgs).toContain('{"a":1}'); + }); + + it("retries 429 responses and keeps 404 non-retryable", async () => { + const rateLimited = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: "rate limited\n429", stderr: "" }), + }); + + await expect( + rateLimited.client.apiRequest("https://registry.example", { + method: "GET", + path: "/v1/ping", + }), + ).rejects.toThrow("rate limited"); + expect(rateLimited.spawnImpl).toHaveBeenCalledTimes(3); + + const missing = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: "missing\n404", stderr: "" }), + }); + await expect( + missing.client.apiRequest("https://registry.example", { + method: "GET", + path: "/v1/ping", + }), + ).rejects.toThrow("missing"); + expect(missing.spawnImpl).toHaveBeenCalledTimes(1); + }); + + it("includes curl rate-limit metadata in 429 errors", async () => { + const { client, spawnImpl } = createBunClient({ + spawnImpl: () => ({ + status: 0, + stdout: "rate limited\n__CLAWHUB_CURL_META__\n429\n20\n0\n1771404540\n20\n0\n34\n34\n", + stderr: "", + }), + }); + + await expect( + client.apiRequest("https://registry.example", { + method: "GET", + path: "/v1/ping", + }), + ).rejects.toThrow(/retry in 34s.*remaining: 0\/20.*reset in 34s/i); + expect(spawnImpl).toHaveBeenCalledTimes(3); + }); + + it("supports fetchText and downloadZip via curl", async () => { + const { client, spawnImpl, readFileImpl, rmImpl } = createBunClient({ + spawnImpl: vi + .fn() + .mockReturnValueOnce({ status: 0, stdout: "hello world\n200", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "200", stderr: "" }) + .mockReturnValueOnce({ status: 0, stdout: "404", stderr: "" }), + mkdtempValue: "/tmp/clawhub-download-abc", + readFileValue: Buffer.from("not found"), + }); + + await expect( + client.fetchText("https://registry.example", { path: "/v1/readme" }), + ).resolves.toBe("hello world"); + const bytes = await client.downloadZip("https://registry.example", { + slug: "demo", + token: "t", + }); + expect(Array.from(bytes)).toEqual(Array.from(Buffer.from("not found"))); + await expect( + client.downloadZip("https://registry.example", { slug: "demo", token: "t" }), + ).rejects.toThrow("not found"); + + expect(readFileImpl).toHaveBeenCalled(); + expect(rmImpl).toHaveBeenCalledWith("/tmp/clawhub-download-abc", { + recursive: true, + force: true, + }); + expect(spawnImpl).toHaveBeenCalledTimes(3); + }); + + it("posts multipart form data via curl and cleans up temp files", async () => { + const { client, spawnImpl, mkdirImpl, writeFileImpl, rmImpl } = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: "" }), + mkdtempValue: "/tmp/clawhub-upload-abc", + }); + + const form = new FormData(); + form.append("name", "demo"); + form.append("file", new Blob(["abc"], { type: "text/plain" }), "dist/demo.txt"); + + const result = await client.apiRequestForm<{ ok: boolean }>("https://registry.example", { + method: "POST", + path: "/upload", + form, + }); + + expect(result).toEqual({ ok: true }); + expect(mkdirImpl).toHaveBeenCalledWith("/tmp/clawhub-upload-abc/dist", { recursive: true }); + expect(writeFileImpl).toHaveBeenCalled(); + expect(rmImpl).toHaveBeenCalledWith("/tmp/clawhub-upload-abc", { + recursive: true, + force: true, + }); + const [, args] = spawnImpl.mock.calls[0] as [string, string[]]; + expect(args).toContain("-F"); + expect(args.some((arg) => arg.includes("name=demo"))).toBe(true); + expect(args.some((arg) => arg.includes("file=@/tmp/clawhub-upload-abc/dist/demo.txt"))).toBe( + true, + ); + }); +}); diff --git a/dt-skill/src/http.test.ts b/dt-skill/src/http.test.ts new file mode 100644 index 00000000..c3923087 --- /dev/null +++ b/dt-skill/src/http.test.ts @@ -0,0 +1,412 @@ +/* @vitest-environment node */ + +import { describe, expect, it, vi } from "vitest"; +import { createHttpClient, detectHttpRuntime, registryUrl, shouldUseProxyFromEnv } from "./http.js"; +import { ApiV1WhoamiResponseSchema } from "./schema/index.js"; + +function createNodeClient(options?: { + fetchImpl?: typeof fetch; + setTimeoutImpl?: typeof setTimeout; + clearTimeoutImpl?: typeof clearTimeout; + now?: () => number; +}) { + return createHttpClient({ + runtime: "node", + configureDispatcher: false, + fetchImpl: options?.fetchImpl, + setTimeoutImpl: options?.setTimeoutImpl, + clearTimeoutImpl: options?.clearTimeoutImpl, + now: options?.now, + random: () => 0, + }); +} + +function createImmediateTimeouts() { + const setTimeoutImpl = vi.fn((callback: () => void, _ms?: number) => { + callback(); + return 1 as unknown as ReturnType; + }); + const clearTimeoutImpl = vi.fn(); + return { setTimeoutImpl, clearTimeoutImpl }; +} + +function createAbortingFetchMock() { + return vi.fn(async (_url: string, init?: RequestInit) => { + const signal = init?.signal; + if (!(signal instanceof AbortSignal)) { + throw new Error("Missing abort signal"); + } + if (signal.aborted) { + throw signal.reason; + } + return await new Promise((_resolve, reject) => { + signal.addEventListener( + "abort", + () => { + reject(signal.reason); + }, + { once: true }, + ); + }); + }); +} + +describe("detectHttpRuntime", () => { + it("detects bun and node runtimes explicitly", () => { + expect(detectHttpRuntime({ bun: "1.2.3" } as unknown as NodeJS.ProcessVersions)).toBe("bun"); + expect(detectHttpRuntime({ node: "22.0.0" } as unknown as NodeJS.ProcessVersions)).toBe("node"); + }); +}); + +describe("shouldUseProxyFromEnv", () => { + it("detects standard proxy variables", () => { + expect( + shouldUseProxyFromEnv({ + HTTPS_PROXY: "http://proxy.example:3128", + } as NodeJS.ProcessEnv), + ).toBe(true); + expect( + shouldUseProxyFromEnv({ + HTTP_PROXY: "http://proxy.example:3128", + } as NodeJS.ProcessEnv), + ).toBe(true); + expect( + shouldUseProxyFromEnv({ + https_proxy: "http://proxy.example:3128", + } as NodeJS.ProcessEnv), + ).toBe(true); + }); + + it("ignores NO_PROXY-only configs", () => { + expect( + shouldUseProxyFromEnv({ + NO_PROXY: "localhost,127.0.0.1", + } as NodeJS.ProcessEnv), + ).toBe(false); + expect(shouldUseProxyFromEnv({} as NodeJS.ProcessEnv)).toBe(false); + }); +}); + +describe("registryUrl", () => { + it("preserves registry base paths and normalizes slashes", () => { + expect(registryUrl("/api/v1/skills", "https://clawhub.ai").toString()).toBe( + "https://clawhub.ai/api/v1/skills", + ); + expect(registryUrl("/api/v1/skills", "http://localhost:8081/custom/path").toString()).toBe( + "http://localhost:8081/custom/path/api/v1/skills", + ); + expect(registryUrl("api/v1/skills", "http://localhost:8081/custom/path/").toString()).toBe( + "http://localhost:8081/custom/path/api/v1/skills", + ); + }); +}); + +describe("node http client", () => { + it("adds bearer token and parses json", async () => { + const fetchImpl = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ user: { handle: null } }), + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + const result = await client.apiRequest( + "https://example.com", + { method: "GET", path: "/x", token: "clh_token" }, + ApiV1WhoamiResponseSchema, + ); + + expect(result.user.handle).toBeNull(); + const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; + expect((init.headers as Record).Authorization).toBe("Bearer clh_token"); + }); + + it("posts json body", async () => { + const fetchImpl = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ ok: true }), + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + await client.apiRequest("https://example.com", { + method: "POST", + path: "/x", + body: { a: 1 }, + }); + + const [url, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; + expect(url).toBe("https://example.com/x"); + expect(init.body).toBe(JSON.stringify({ a: 1 })); + expect((init.headers as Record)["Content-Type"]).toBe("application/json"); + }); + + it("includes rate-limit guidance from response headers on 429", async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + headers: new Headers({ + "Retry-After": "34", + "X-RateLimit-Limit": "20", + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": "1771404540", + }), + text: async () => "Rate limit exceeded", + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/x" }), + ).rejects.toThrow(/retry in 34s.*remaining: 0\/20.*reset in 34s/i); + expect(fetchImpl).toHaveBeenCalledTimes(3); + expect(clearTimeoutImpl).toHaveBeenCalledTimes(3); + }); + + it("interprets legacy epoch Retry-After values as reset delays", async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + headers: new Headers({ + "Retry-After": "1771404540", + "X-RateLimit-Limit": "20", + "X-RateLimit-Remaining": "0", + }), + text: async () => "Rate limit exceeded", + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + now: () => 1_771_404_500_000, + }); + + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/x" }), + ).rejects.toThrow(/retry in 40s.*remaining: 0\/20/i); + }); + + it("falls back to HTTP status when response bodies are empty", async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + text: async () => "", + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequest("https://example.com", { method: "GET", url: "https://example.com/x" }), + ).rejects.toThrow("HTTP 500"); + expect(fetchImpl).toHaveBeenCalledTimes(3); + }); + + it("retries and labels transient Convex write contention", async () => { + const contention = + 'Documents read from or written to the "publishers" table changed while this mutation was being run'; + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 400, + text: async () => contention, + }) + .mockResolvedValueOnce({ + ok: false, + status: 400, + text: async () => contention, + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ ok: true }), + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequestForm("https://example.com", { + method: "POST", + path: "/upload", + form: new FormData(), + retryCount: 5, + }), + ).resolves.toEqual({ ok: true }); + expect(fetchImpl).toHaveBeenCalledTimes(3); + + const failingFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 400, + text: async () => contention, + }); + const failingClient = createNodeClient({ + fetchImpl: failingFetch as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + await expect( + failingClient.apiRequestForm("https://example.com", { + method: "POST", + path: "/upload", + form: new FormData(), + retryCount: 0, + }), + ).rejects.toThrow(/Transient ClawHub write contention.*package artifact passed/i); + }); + + it("expands generic auth and visibility failures into actionable messages", async () => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers(), + text: async () => "Unauthorized", + }) + .mockResolvedValueOnce({ + ok: false, + status: 403, + headers: new Headers(), + text: async () => "Forbidden", + }) + .mockResolvedValueOnce({ + ok: false, + status: 404, + headers: new Headers(), + text: async () => "Package not found", + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/auth" }), + ).rejects.toThrow(/clawhub login.*deleted, banned, or disabled/i); + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/forbidden" }), + ).rejects.toThrow(/account does not have access.*not in good standing/i); + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/missing" }), + ).rejects.toThrow(/Package not found or not visible to this account/i); + }); + + it("downloads zip bytes and does not retry non-retryable errors", async () => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, + }) + .mockResolvedValueOnce({ + ok: false, + status: 404, + text: async () => "nope", + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + const bytes = await client.downloadZip("https://example.com", { + slug: "demo", + version: "1.0.0", + token: "clh_token", + }); + expect(Array.from(bytes)).toEqual([1, 2, 3]); + + await expect(client.downloadZip("https://example.com", { slug: "demo" })).rejects.toThrow( + "nope", + ); + expect(fetchImpl).toHaveBeenCalledTimes(2); + }); + + it("retries request and text timeouts using injected timeout helpers", async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = createAbortingFetchMock(); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/x" }), + ).rejects.toThrow(/timed out/i); + await expect(client.fetchText("https://example.com", { path: "/x" })).rejects.toThrow( + /timed out/i, + ); + expect(fetchImpl).toHaveBeenCalledTimes(6); + expect(clearTimeoutImpl).toHaveBeenCalledTimes(6); + }); + + it("normalizes non-Error throws from fetch", async () => { + const fetchImpl = vi.fn(async () => { + throw { message: "The operation was aborted", name: "AbortError" }; + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + await expect( + client.apiRequest("https://example.com", { method: "GET", path: "/x" }), + ).rejects.toThrow("The operation was aborted"); + }); + + it("posts form data, retries 429, and uses the upload timeout", async () => { + const successFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ ok: true }), + }); + const successClient = createNodeClient({ fetchImpl: successFetch as unknown as typeof fetch }); + const form = new FormData(); + form.append("x", "1"); + const result = await successClient.apiRequestForm("https://example.com", { + method: "POST", + path: "/upload", + token: "clh_token", + form, + }); + expect(result).toEqual({ ok: true }); + const [, init] = successFetch.mock.calls[0] as [string, RequestInit]; + expect(init.body).toBe(form); + expect((init.headers as Record).Authorization).toBe("Bearer clh_token"); + + const rateLimitedFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + text: async () => "rate limited", + }); + const retryClient = createNodeClient({ + fetchImpl: rateLimitedFetch as unknown as typeof fetch, + setTimeoutImpl: createImmediateTimeouts().setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl: vi.fn(), + }); + await expect( + retryClient.apiRequestForm("https://example.com", { + method: "POST", + path: "/upload", + form: new FormData(), + }), + ).rejects.toThrow("rate limited"); + expect(rateLimitedFetch).toHaveBeenCalledTimes(3); + + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const abortingFetch = createAbortingFetchMock(); + const timeoutClient = createNodeClient({ + fetchImpl: abortingFetch as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + await expect( + timeoutClient.apiRequestForm("https://example.com", { + method: "POST", + path: "/upload", + form: new FormData(), + }), + ).rejects.toThrow(/timed out after 120s/i); + expect(setTimeoutImpl.mock.calls[0]?.[1]).toBe(120_000); + }); +}); diff --git a/dt-skill/src/http.ts b/dt-skill/src/http.ts new file mode 100644 index 00000000..f5a21dbb --- /dev/null +++ b/dt-skill/src/http.ts @@ -0,0 +1,827 @@ +import { spawnSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import pRetry, { AbortError } from "p-retry"; +import { Agent, EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; +import type { ArkValidator } from "./schema/index.js"; +import { ApiRoutes, parseArk } from "./schema/index.js"; + +const REQUEST_TIMEOUT_MS = 15_000; +const UPLOAD_TIMEOUT_MS = 120_000; +const REQUEST_TIMEOUT_SECONDS = Math.ceil(REQUEST_TIMEOUT_MS / 1000); +const UPLOAD_TIMEOUT_SECONDS = Math.ceil(UPLOAD_TIMEOUT_MS / 1000); +const RETRY_COUNT = 2; +const RETRY_BACKOFF_BASE_MS = 300; +const RETRY_BACKOFF_MAX_MS = 5_000; +const RETRY_AFTER_JITTER_MS = 250; +const CURL_META_MARKER = "__CLAWHUB_CURL_META__"; +const CURL_WRITE_OUT_FORMAT = [ + "", + CURL_META_MARKER, + "%{http_code}", + "%{header:x-ratelimit-limit}", + "%{header:x-ratelimit-remaining}", + "%{header:x-ratelimit-reset}", + "%{header:ratelimit-limit}", + "%{header:ratelimit-remaining}", + "%{header:ratelimit-reset}", + "%{header:retry-after}", +].join("\n"); + +export type HttpRuntime = "node" | "bun"; + +type RequestArgs = + | { + method: "GET" | "POST" | "DELETE"; + path: string; + token?: string; + body?: unknown; + retryCount?: number; + } + | { + method: "GET" | "POST" | "DELETE"; + url: string; + token?: string; + body?: unknown; + retryCount?: number; + }; + +type FormRequestArgs = + | { method: "POST"; path: string; token?: string; form: FormData; retryCount?: number } + | { method: "POST"; url: string; token?: string; form: FormData; retryCount?: number }; + +type TextRequestArgs = { path: string; token?: string } | { url: string; token?: string }; + +type HeaderSource = Headers | Record | null | undefined; + +type RateLimitInfo = { + limit?: number; + remaining?: number; + resetDelaySeconds?: number; + retryAfterSeconds?: number; +}; + +type HttpClientDeps = { + runtime: HttpRuntime; + fetchImpl: typeof fetch; + setTimeoutImpl: typeof setTimeout; + clearTimeoutImpl: typeof clearTimeout; + spawnSyncImpl: typeof spawnSync; + mkdirImpl: typeof mkdir; + mkdtempImpl: typeof mkdtemp; + readFileImpl: typeof readFile; + rmImpl: typeof rm; + writeFileImpl: typeof writeFile; + tmpdirPath: string; + now: () => number; + random: () => number; + env: NodeJS.ProcessEnv; + configureDispatcher: boolean; +}; + +export type HttpClientOptions = Partial> & { + runtime?: HttpRuntime; +}; + +type HttpClient = { + apiRequest(registry: string, args: RequestArgs): Promise; + apiRequest(registry: string, args: RequestArgs, schema: ArkValidator): Promise; + apiRequestForm(registry: string, args: FormRequestArgs): Promise; + apiRequestForm(registry: string, args: FormRequestArgs, schema: ArkValidator): Promise; + fetchText(registry: string, args: TextRequestArgs): Promise; + fetchBinary(registry: string, args: TextRequestArgs): Promise; + downloadZip( + registry: string, + args: { slug: string; version?: string; token?: string }, + ): Promise; +}; + +class HttpStatusError extends Error { + readonly status: number; + readonly rateLimit: RateLimitInfo; + + constructor(status: number, message: string, rateLimit: RateLimitInfo) { + super(message); + this.name = "HttpStatusError"; + this.status = status; + this.rateLimit = rateLimit; + } +} + +export function detectHttpRuntime( + processVersions: NodeJS.ProcessVersions | undefined = process.versions, +): HttpRuntime { + return processVersions?.bun ? "bun" : "node"; +} + +export function shouldUseProxyFromEnv(env: NodeJS.ProcessEnv = process.env): boolean { + return Boolean(env.HTTPS_PROXY || env.HTTP_PROXY || env.https_proxy || env.http_proxy); +} + +export function registryUrl(path: string, registry: string): URL { + const base = registry.endsWith("/") ? registry : `${registry}/`; + const relative = path.startsWith("/") ? path.slice(1) : path; + return new URL(relative, base); +} + +export function createHttpClient(options: HttpClientOptions = {}): HttpClient { + const deps: HttpClientDeps = { + runtime: options.runtime ?? detectHttpRuntime(), + fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis), + setTimeoutImpl: options.setTimeoutImpl ?? globalThis.setTimeout.bind(globalThis), + clearTimeoutImpl: options.clearTimeoutImpl ?? globalThis.clearTimeout.bind(globalThis), + spawnSyncImpl: options.spawnSyncImpl ?? spawnSync, + mkdirImpl: options.mkdirImpl ?? mkdir, + mkdtempImpl: options.mkdtempImpl ?? mkdtemp, + readFileImpl: options.readFileImpl ?? readFile, + rmImpl: options.rmImpl ?? rm, + writeFileImpl: options.writeFileImpl ?? writeFile, + tmpdirPath: options.tmpdirPath ?? tmpdir(), + now: options.now ?? Date.now, + random: options.random ?? Math.random, + env: options.env ?? process.env, + configureDispatcher: options.configureDispatcher ?? true, + }; + + if (deps.runtime === "node" && deps.configureDispatcher) { + configureNodeDispatcher(deps.env); + } + + const runWithRetries = createRetryRunner(deps); + + async function apiRequest( + registry: string, + args: RequestArgs, + schema?: ArkValidator, + ): Promise { + const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); + const json = await runWithRetries(async () => { + if (deps.runtime === "bun") { + return await fetchJsonViaCurl(deps, url, args); + } + + const headers: Record = { Accept: "application/json" }; + if (args.token) headers.Authorization = `Bearer ${args.token}`; + let body: string | undefined; + if (args.body !== undefined || args.method === "POST") { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(args.body ?? {}); + } + const response = await fetchWithTimeout(deps, url, { + method: args.method, + headers, + body, + }); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now, + ); + } + return (await response.json()) as unknown; + }, args.retryCount); + if (schema) return parseArk(schema, json, "API response"); + return json as T; + } + + async function apiRequestForm( + registry: string, + args: FormRequestArgs, + schema?: ArkValidator, + ): Promise { + const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); + const json = await runWithRetries(async () => { + if (deps.runtime === "bun") { + return await fetchJsonFormViaCurl(deps, url, args); + } + + const headers: Record = { Accept: "application/json" }; + if (args.token) headers.Authorization = `Bearer ${args.token}`; + const response = await fetchWithTimeout( + deps, + url, + { + method: args.method, + headers, + body: args.form, + }, + UPLOAD_TIMEOUT_MS, + ); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now, + ); + } + return (await response.json()) as unknown; + }, args.retryCount); + if (schema) return parseArk(schema, json, "API response"); + return json as T; + } + + async function fetchTextRequest(registry: string, args: TextRequestArgs): Promise { + const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); + return await runWithRetries(async () => { + if (deps.runtime === "bun") { + return await fetchTextViaCurl(deps, url, args); + } + + const headers: Record = { Accept: "text/plain" }; + if (args.token) headers.Authorization = `Bearer ${args.token}`; + const response = await fetchWithTimeout(deps, url, { method: "GET", headers }); + const text = await response.text(); + if (!response.ok) { + throwHttpStatusError(response.status, text, response.headers, deps.now); + } + return text; + }); + } + + async function fetchBinaryRequest(registry: string, args: TextRequestArgs): Promise { + const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); + return await runWithRetries(async () => { + if (deps.runtime === "bun") { + return await fetchBinaryViaCurl(deps, url, args.token); + } + + const headers: Record = {}; + if (args.token) headers.Authorization = `Bearer ${args.token}`; + const response = await fetchWithTimeout(deps, url, { method: "GET", headers }); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now, + ); + } + return new Uint8Array(await response.arrayBuffer()); + }); + } + + async function downloadZipRequest( + registry: string, + args: { slug: string; version?: string; token?: string }, + ) { + const url = registryUrl(ApiRoutes.download, registry); + url.searchParams.set("slug", args.slug); + if (args.version) url.searchParams.set("version", args.version); + return await runWithRetries(async () => { + if (deps.runtime === "bun") { + return await fetchBinaryViaCurl(deps, url.toString(), args.token); + } + + const headers: Record = {}; + if (args.token) headers.Authorization = `Bearer ${args.token}`; + const response = await fetchWithTimeout(deps, url.toString(), { method: "GET", headers }); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now, + ); + } + return new Uint8Array(await response.arrayBuffer()); + }); + } + + return { + apiRequest, + apiRequestForm, + fetchText: fetchTextRequest, + fetchBinary: fetchBinaryRequest, + downloadZip: downloadZipRequest, + }; +} + +function configureNodeDispatcher(env: NodeJS.ProcessEnv) { + if (!process.versions?.node) return; + try { + setGlobalDispatcher( + shouldUseProxyFromEnv(env) + ? new EnvHttpProxyAgent({ + connect: { timeout: REQUEST_TIMEOUT_MS }, + }) + : new Agent({ + connect: { timeout: REQUEST_TIMEOUT_MS }, + }), + ); + } catch { + // Ignore dispatcher setup failures in environments that partially emulate Node APIs. + } +} + +const defaultHttpClient = createHttpClient(); + +export async function apiRequest(registry: string, args: RequestArgs): Promise; +export async function apiRequest( + registry: string, + args: RequestArgs, + schema: ArkValidator, +): Promise; +export async function apiRequest( + registry: string, + args: RequestArgs, + schema?: ArkValidator, +): Promise { + if (schema) { + return await defaultHttpClient.apiRequest(registry, args, schema); + } + return await defaultHttpClient.apiRequest(registry, args); +} + +export async function apiRequestForm(registry: string, args: FormRequestArgs): Promise; +export async function apiRequestForm( + registry: string, + args: FormRequestArgs, + schema: ArkValidator, +): Promise; +export async function apiRequestForm( + registry: string, + args: FormRequestArgs, + schema?: ArkValidator, +): Promise { + if (schema) { + return await defaultHttpClient.apiRequestForm(registry, args, schema); + } + return await defaultHttpClient.apiRequestForm(registry, args); +} + +export async function fetchText(registry: string, args: TextRequestArgs): Promise { + return await defaultHttpClient.fetchText(registry, args); +} + +export async function fetchBinary(registry: string, args: TextRequestArgs): Promise { + return await defaultHttpClient.fetchBinary(registry, args); +} + +export async function downloadZip( + registry: string, + args: { slug: string; version?: string; token?: string }, +) { + return await defaultHttpClient.downloadZip(registry, args); +} + +function createRetryRunner(deps: Pick) { + return async function runWithRetries( + fn: () => Promise, + retryCount = RETRY_COUNT, + ): Promise { + return await pRetry(fn, { + retries: retryCount, + minTimeout: 0, + maxTimeout: 0, + factor: 1, + randomize: false, + onFailedAttempt: async (attemptError) => { + const delayMs = getRetryDelayMs(attemptError, deps.random); + if (delayMs <= 0) return; + await sleep(delayMs, deps.setTimeoutImpl); + }, + }); + }; +} + +async function fetchWithTimeout( + deps: Pick, + url: string, + init: RequestInit, + timeoutMs = REQUEST_TIMEOUT_MS, +): Promise { + const controller = new AbortController(); + const timeoutSeconds = Math.ceil(timeoutMs / 1000); + const timeout = deps.setTimeoutImpl( + () => controller.abort(new Error(`Request timed out after ${timeoutSeconds}s`)), + timeoutMs, + ); + try { + return await deps.fetchImpl(url, { ...init, signal: controller.signal }); + } catch (error) { + if (error instanceof Error) throw error; + const message = + typeof error === "object" && error !== null && "message" in error + ? String((error as { message: unknown }).message) + : String(error); + throw new Error(message, { cause: error }); + } finally { + deps.clearTimeoutImpl(timeout); + } +} + +async function readResponseTextSafe(response: Response): Promise { + return await response.text().catch(() => ""); +} + +function getRetryDelayMs(attemptError: unknown, random: () => number): number { + const failed = attemptError as { + attemptNumber?: number; + cause?: unknown; + error?: unknown; + }; + const attemptNumber = Math.max(1, failed.attemptNumber ?? 1); + const rootError = failed.cause ?? failed.error ?? attemptError; + if (rootError instanceof HttpStatusError && rootError.rateLimit.retryAfterSeconds !== undefined) { + return rootError.rateLimit.retryAfterSeconds * 1000 + jitterMs(RETRY_AFTER_JITTER_MS, random); + } + const baseMs = Math.min(RETRY_BACKOFF_MAX_MS, RETRY_BACKOFF_BASE_MS * 2 ** (attemptNumber - 1)); + return baseMs + jitterMs(RETRY_BACKOFF_BASE_MS, random); +} + +function sleep(ms: number, setTimeoutImpl: typeof setTimeout): Promise { + return new Promise((resolve) => { + setTimeoutImpl(resolve, ms); + }); +} + +function jitterMs(maxMs: number, random: () => number): number { + if (maxMs <= 0) return 0; + return Math.floor(random() * maxMs); +} + +function throwHttpStatusError( + status: number, + text: string, + headers: HeaderSource, + now: () => number, +): never { + const rateLimit = parseRateLimitInfo(headers, now); + const retryableTransientContention = isTransientConvexContention(text); + const message = buildHttpErrorMessage(status, text, rateLimit); + if (status === 429 || status >= 500 || retryableTransientContention) { + throw new HttpStatusError(status, message, rateLimit); + } + throw new AbortError(message); +} + +function buildHttpErrorMessage(status: number, text: string, rateLimit: RateLimitInfo): string { + const base = normalizeHttpErrorBody(status, text); + const details: string[] = []; + if (rateLimit.retryAfterSeconds !== undefined) { + details.push(`retry in ${rateLimit.retryAfterSeconds}s`); + } + if (rateLimit.remaining !== undefined && rateLimit.limit !== undefined) { + details.push(`remaining: ${rateLimit.remaining}/${rateLimit.limit}`); + } + if (rateLimit.resetDelaySeconds !== undefined) { + details.push(`reset in ${rateLimit.resetDelaySeconds}s`); + } + return details.length === 0 ? base : `${base} (${details.join(", ")})`; +} + +function normalizeHttpErrorBody(status: number, text: string): string { + const body = text.trim(); + const lowered = body.toLowerCase(); + if (body && lowered !== "unauthorized" && lowered !== "forbidden") { + if (isTransientConvexContention(body)) { + return `Transient ClawHub write contention. The package artifact passed request validation; retrying usually succeeds. ${body}`; + } + if (status === 404 && lowered === "package not found") { + return "Package not found or not visible to this account."; + } + if (status === 404 && lowered === "skill not found") { + return "Skill not found or unavailable to this account."; + } + return body; + } + if (status === 401) { + return "Authentication failed. Run `clawhub login` again. Deleted, banned, or disabled ClawHub accounts cannot use API tokens."; + } + if (status === 403) { + return "Permission denied. This account does not have access to this operation, or the account is not in good standing."; + } + if (body) return body; + return `HTTP ${status}`; +} + +function isTransientConvexContention(text: string) { + const lowered = text.toLowerCase(); + return ( + lowered.includes("optimistic concurrency") || + lowered.includes("write conflict") || + (lowered.includes('documents read from or written to the "') && + lowered.includes("changed while this mutation was being run")) + ); +} + +function parseRateLimitInfo(headers: HeaderSource, now: () => number): RateLimitInfo { + if (!headers) return {}; + const limit = parseIntHeader( + getHeader(headers, "x-ratelimit-limit") ?? getHeader(headers, "ratelimit-limit"), + ); + const remaining = parseIntHeader( + getHeader(headers, "x-ratelimit-remaining") ?? getHeader(headers, "ratelimit-remaining"), + ); + const nowMs = now(); + const retryAfterSeconds = parseRetryAfterSeconds(getHeader(headers, "retry-after"), nowMs); + const resetDelaySeconds = parseResetDelaySeconds(headers, nowMs, retryAfterSeconds); + return { limit, remaining, resetDelaySeconds, retryAfterSeconds }; +} + +function parseResetDelaySeconds( + headers: HeaderSource, + nowMs: number, + retryAfterSeconds: number | undefined, +): number | undefined { + if (retryAfterSeconds !== undefined) return retryAfterSeconds; + const standardized = parseIntHeader(getHeader(headers, "ratelimit-reset")); + if (standardized !== undefined) { + return Math.max(1, standardized); + } + const legacyEpochSeconds = parseIntHeader(getHeader(headers, "x-ratelimit-reset")); + if (legacyEpochSeconds === undefined) return undefined; + const nowSeconds = Math.floor(nowMs / 1000); + return Math.max(1, legacyEpochSeconds - nowSeconds); +} + +function parseRetryAfterSeconds(value: string | undefined, nowMs: number): number | undefined { + if (!value) return undefined; + const trimmed = value.trim(); + if (!trimmed) return undefined; + + const asNumber = Number(trimmed); + if (Number.isFinite(asNumber) && asNumber >= 0) { + if (asNumber > 31_536_000) { + const nowSeconds = Math.floor(nowMs / 1000); + return Math.max(1, Math.ceil(asNumber - nowSeconds)); + } + return Math.max(1, Math.ceil(asNumber)); + } + + const asDateMs = Date.parse(trimmed); + if (!Number.isFinite(asDateMs)) return undefined; + return Math.max(1, Math.ceil((asDateMs - nowMs) / 1000)); +} + +function parseIntHeader(value: string | undefined): number | undefined { + if (!value) return undefined; + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function getHeader(headers: HeaderSource, key: string): string | undefined { + if (!headers) return undefined; + if (headers instanceof Headers) { + const value = headers.get(key); + return value === null ? undefined : value; + } + const normalizedKey = key.toLowerCase(); + const direct = headers[normalizedKey] ?? headers[key]; + if (typeof direct === "string" && direct.trim()) return direct.trim(); + const match = Object.entries(headers).find( + ([entryKey, entryValue]) => + entryKey.toLowerCase() === normalizedKey && + typeof entryValue === "string" && + entryValue.trim(), + ); + return typeof match?.[1] === "string" ? match[1].trim() : undefined; +} + +async function fetchJsonViaCurl( + deps: Pick, + url: string, + args: RequestArgs, +) { + const headers = ["-H", "Accept: application/json"]; + if (args.token) headers.push("-H", `Authorization: Bearer ${args.token}`); + const curlArgs = [ + "--silent", + "--show-error", + "--location", + "--max-time", + String(REQUEST_TIMEOUT_SECONDS), + "--write-out", + CURL_WRITE_OUT_FORMAT, + "-X", + args.method, + ...headers, + url, + ]; + if (args.body !== undefined || args.method === "POST") { + curlArgs.push("-H", "Content-Type: application/json"); + curlArgs.push("--data-binary", JSON.stringify(args.body ?? {})); + } + + const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); + if (result.status !== 0) { + throw new Error(result.stderr || "curl failed"); + } + const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); + if (status < 200 || status >= 300) { + throwHttpStatusError(status, body, responseHeaders, deps.now); + } + return JSON.parse(body || "null") as unknown; +} + +async function fetchJsonFormViaCurl( + deps: Pick< + HttpClientDeps, + | "spawnSyncImpl" + | "mkdtempImpl" + | "mkdirImpl" + | "writeFileImpl" + | "rmImpl" + | "tmpdirPath" + | "now" + >, + url: string, + args: FormRequestArgs, +) { + const headers = ["-H", "Accept: application/json"]; + if (args.token) headers.push("-H", `Authorization: Bearer ${args.token}`); + + const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "clawhub-upload-")); + try { + const formArgs: string[] = []; + for (const [key, value] of args.form.entries()) { + if (value instanceof Blob) { + const filename = typeof (value as File).name === "string" ? (value as File).name : "file"; + const filePath = join(tempDir, filename); + const bytes = new Uint8Array(await value.arrayBuffer()); + await deps.mkdirImpl(dirname(filePath), { recursive: true }); + await deps.writeFileImpl(filePath, bytes); + formArgs.push("-F", `${key}=@${filePath};filename=${filename}`); + } else { + formArgs.push("-F", `${key}=${value}`); + } + } + + const curlArgs = [ + "--silent", + "--show-error", + "--location", + "--max-time", + String(UPLOAD_TIMEOUT_SECONDS), + "--write-out", + CURL_WRITE_OUT_FORMAT, + "-X", + args.method, + ...headers, + ...formArgs, + url, + ]; + + const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); + if (result.status !== 0) { + throw new Error(result.stderr || "curl failed"); + } + const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); + if (status < 200 || status >= 300) { + throwHttpStatusError(status, body, responseHeaders, deps.now); + } + return JSON.parse(body || "null") as unknown; + } finally { + await deps.rmImpl(tempDir, { recursive: true, force: true }); + } +} + +async function fetchTextViaCurl( + deps: Pick, + url: string, + args: { token?: string }, +) { + const headers = ["-H", "Accept: text/plain"]; + if (args.token) headers.push("-H", `Authorization: Bearer ${args.token}`); + const curlArgs = [ + "--silent", + "--show-error", + "--location", + "--max-time", + String(REQUEST_TIMEOUT_SECONDS), + "--write-out", + CURL_WRITE_OUT_FORMAT, + "-X", + "GET", + ...headers, + url, + ]; + const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); + if (result.status !== 0) { + throw new Error(result.stderr || "curl failed"); + } + const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); + if (status < 200 || status >= 300) { + throwHttpStatusError(status, body, responseHeaders, deps.now); + } + return body; +} + +async function fetchBinaryViaCurl( + deps: Pick< + HttpClientDeps, + "spawnSyncImpl" | "mkdtempImpl" | "readFileImpl" | "rmImpl" | "tmpdirPath" | "now" + >, + url: string, + token?: string, +) { + const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "clawhub-download-")); + const filePath = join(tempDir, "payload.bin"); + try { + const headers: string[] = []; + if (token) headers.push("-H", `Authorization: Bearer ${token}`); + + const curlArgs = [ + "--silent", + "--show-error", + "--location", + "--max-time", + String(REQUEST_TIMEOUT_SECONDS), + ...headers, + "-o", + filePath, + "--write-out", + CURL_WRITE_OUT_FORMAT, + url, + ]; + const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); + if (result.status !== 0) { + throw new Error(result.stderr || "curl failed"); + } + const { status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); + if (status < 200 || status >= 300) { + const body = await readFileSafe(deps.readFileImpl, filePath); + throwHttpStatusError( + status, + body ? new TextDecoder().decode(body) : "", + responseHeaders, + deps.now, + ); + } + const bytes = await readFileSafe(deps.readFileImpl, filePath); + return bytes ? new Uint8Array(bytes) : new Uint8Array(); + } finally { + await deps.rmImpl(tempDir, { recursive: true, force: true }); + } +} + +function parseCurlBodyAndMeta(output: string): { + body: string; + status: number; + headers: Record; +} { + const marker = `\n${CURL_META_MARKER}\n`; + const markerIndex = output.lastIndexOf(marker); + if (markerIndex === -1) { + const splitAt = output.lastIndexOf("\n"); + if (splitAt === -1) { + const statusOnly = Number(output.trim()); + if (!Number.isFinite(statusOnly)) throw new Error("curl response missing status"); + return { body: "", status: statusOnly, headers: {} }; + } + const body = output.slice(0, splitAt); + const status = Number(output.slice(splitAt + 1).trim()); + if (!Number.isFinite(status)) throw new Error("curl response missing status"); + return { body, status, headers: {} }; + } + + const body = output.slice(0, markerIndex); + const meta = output.slice(markerIndex + marker.length).replace(/\r/g, ""); + const lines = meta.split("\n"); + const status = Number((lines[0] ?? "").trim()); + if (!Number.isFinite(status)) throw new Error("curl response missing status"); + + const [ + xRateLimitLimit, + xRateLimitRemaining, + xRateLimitReset, + rateLimitLimit, + rateLimitRemaining, + rateLimitReset, + retryAfter, + ] = lines.slice(1); + + const headers: Record = {}; + setHeaderIfPresent(headers, "x-ratelimit-limit", xRateLimitLimit); + setHeaderIfPresent(headers, "x-ratelimit-remaining", xRateLimitRemaining); + setHeaderIfPresent(headers, "x-ratelimit-reset", xRateLimitReset); + setHeaderIfPresent(headers, "ratelimit-limit", rateLimitLimit); + setHeaderIfPresent(headers, "ratelimit-remaining", rateLimitRemaining); + setHeaderIfPresent(headers, "ratelimit-reset", rateLimitReset); + setHeaderIfPresent(headers, "retry-after", retryAfter); + + return { body, status, headers }; +} + +function setHeaderIfPresent( + headers: Record, + key: string, + value: string | undefined, +) { + if (typeof value !== "string") return; + const trimmed = value.trim(); + if (!trimmed) return; + headers[key] = trimmed; +} + +async function readFileSafe(readFileImpl: typeof readFile, path: string) { + try { + return await readFileImpl(path); + } catch { + return null; + } +} diff --git a/dt-skill/src/schema/ark.ts b/dt-skill/src/schema/ark.ts new file mode 100644 index 00000000..335096b5 --- /dev/null +++ b/dt-skill/src/schema/ark.ts @@ -0,0 +1,29 @@ +import { ArkErrors } from "arktype"; + +export type ArkValidator = (data: unknown) => T | ArkErrors; + +export function parseArk(schema: ArkValidator, data: unknown, label: string) { + const result = schema(data); + if (result instanceof ArkErrors) { + throw new Error(`${label}: ${formatArkErrors(result)}`); + } + return result; +} + +export function formatArkErrors(errors: ArkErrors) { + const parts: string[] = []; + for (const error of errors) { + if (parts.length >= 3) break; + const path = Array.isArray(error.path) ? error.path.join(".") : ""; + const location = path ? `${path}: ` : ""; + const description = + typeof (error as { description?: unknown }).description === "string" + ? ((error as { description: string }).description as string) + : "invalid value"; + parts.push(`${location}${description}`); + } + if (errors.count > parts.length) { + parts.push(`+${errors.count - parts.length} more`); + } + return parts.join("; "); +} diff --git a/dt-skill/src/schema/clawScanNote.ts b/dt-skill/src/schema/clawScanNote.ts new file mode 100644 index 00000000..7d296b59 --- /dev/null +++ b/dt-skill/src/schema/clawScanNote.ts @@ -0,0 +1,10 @@ +export const MAX_CLAWSCAN_NOTE_CHARS = 4000; + +export function normalizeClawScanNote(value: string | null | undefined) { + const trimmed = value?.trim() ?? ""; + if (!trimmed) return undefined; + if (trimmed.length > MAX_CLAWSCAN_NOTE_CHARS) { + throw new Error(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); + } + return trimmed; +} diff --git a/dt-skill/src/schema/index.ts b/dt-skill/src/schema/index.ts new file mode 100644 index 00000000..4c2ae22e --- /dev/null +++ b/dt-skill/src/schema/index.ts @@ -0,0 +1,9 @@ +export type { ArkValidator } from "./ark.js"; +export { parseArk } from "./ark.js"; +export * from "./clawScanNote.js"; +export { PLATFORM_SKILL_LICENSE, PLATFORM_SKILL_LICENSE_SUMMARY } from "./license.js"; +export * from "./openclawContract.js"; +export * from "./packages.js"; +export { ApiRoutes, LegacyApiRoutes } from "./routes.js"; +export * from "./schemas.js"; +export * from "./textFiles.js"; diff --git a/dt-skill/src/schema/license.ts b/dt-skill/src/schema/license.ts new file mode 100644 index 00000000..4036c036 --- /dev/null +++ b/dt-skill/src/schema/license.ts @@ -0,0 +1,5 @@ +export const PLATFORM_SKILL_LICENSE = "MIT-0" as const; +export const PLATFORM_SKILL_LICENSE_NAME = "MIT No Attribution" as const; +export const PLATFORM_SKILL_LICENSE_SUMMARY = + "Free to use, modify, and redistribute. No attribution required." as const; +export const PLATFORM_SKILL_LICENSE_URL = "https://spdx.org/licenses/MIT-0.html" as const; diff --git a/dt-skill/src/schema/openclawContract.ts b/dt-skill/src/schema/openclawContract.ts new file mode 100644 index 00000000..126d2098 --- /dev/null +++ b/dt-skill/src/schema/openclawContract.ts @@ -0,0 +1,163 @@ +import type { PackageCompatibility } from "./packages.js"; + +type JsonObject = Record; + +export type OpenClawExternalPluginValidationIssue = { + fieldPath: string; + message: string; +}; + +export type OpenClawExternalCodePluginValidation = { + compatibility?: PackageCompatibility; + issues: OpenClawExternalPluginValidationIssue[]; +}; + +export const OPENCLAW_EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [ + "openclaw.compat.pluginApi", + "openclaw.build.openclawVersion", +] as const; +const COMPILED_RUNTIME_EXTENSIONS = [".js", ".mjs", ".cjs"] as const; + +function isRecord(value: unknown): value is JsonObject { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function getTrimmedString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function getTrimmedStringArray(value: unknown): string[] { + return Array.isArray(value) + ? value + .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) + .map((entry) => entry.trim()) + : []; +} + +function normalizePackagePath(value: string): string { + return value.trim().replaceAll("\\", "/").replace(/^\.\//, ""); +} + +function isTypeScriptRuntimeEntry(value: string): boolean { + return /\.(?:c|m)?ts$/u.test(value); +} + +function compiledRuntimeCandidates(entry: string): string[] { + const normalized = normalizePackagePath(entry); + const withoutExtension = normalized.replace(/\.[^.]+$/u, ""); + const distBase = normalized.startsWith("src/") + ? `dist/${normalized.slice("src/".length).replace(/\.[^.]+$/u, "")}` + : `dist/${withoutExtension}`; + return [ + ...COMPILED_RUNTIME_EXTENSIONS.map((ext) => `${distBase}${ext}`), + ...COMPILED_RUNTIME_EXTENSIONS.map((ext) => `${withoutExtension}${ext}`), + ]; +} + +function readOpenClawBlock(packageJson: unknown) { + const root = isRecord(packageJson) ? packageJson : undefined; + const openclaw = isRecord(root?.openclaw) ? root.openclaw : undefined; + const compat = isRecord(openclaw?.compat) ? openclaw.compat : undefined; + const build = isRecord(openclaw?.build) ? openclaw.build : undefined; + const install = isRecord(openclaw?.install) ? openclaw.install : undefined; + return { root, openclaw, compat, build, install }; +} + +export function normalizeOpenClawExternalPluginCompatibility( + packageJson: unknown, +): PackageCompatibility | undefined { + const { root, compat, build, install } = readOpenClawBlock(packageJson); + const version = getTrimmedString(root?.version); + const minHostVersion = getTrimmedString(install?.minHostVersion); + const compatibility: PackageCompatibility = {}; + + const pluginApi = getTrimmedString(compat?.pluginApi); + if (pluginApi) { + compatibility.pluginApiRange = pluginApi; + } + + const minGatewayVersion = getTrimmedString(compat?.minGatewayVersion) ?? minHostVersion; + if (minGatewayVersion) { + compatibility.minGatewayVersion = minGatewayVersion; + } + + const builtWithOpenClawVersion = getTrimmedString(build?.openclawVersion) ?? version; + if (builtWithOpenClawVersion) { + compatibility.builtWithOpenClawVersion = builtWithOpenClawVersion; + } + + const pluginSdkVersion = getTrimmedString(build?.pluginSdkVersion); + if (pluginSdkVersion) { + compatibility.pluginSdkVersion = pluginSdkVersion; + } + + return Object.keys(compatibility).length > 0 ? compatibility : undefined; +} + +export function listMissingOpenClawExternalCodePluginFieldPaths(packageJson: unknown): string[] { + const { compat, build } = readOpenClawBlock(packageJson); + const missing: string[] = []; + if (!getTrimmedString(compat?.pluginApi)) { + missing.push("openclaw.compat.pluginApi"); + } + if (!getTrimmedString(build?.openclawVersion)) { + missing.push("openclaw.build.openclawVersion"); + } + return missing; +} + +export function validateOpenClawExternalCodePluginPackageJson( + packageJson: unknown, +): OpenClawExternalCodePluginValidation { + const issues = listMissingOpenClawExternalCodePluginFieldPaths(packageJson).map((fieldPath) => ({ + fieldPath, + message: `${fieldPath} is required for external code plugins published to ClawHub.`, + })); + return { + compatibility: normalizeOpenClawExternalPluginCompatibility(packageJson), + issues, + }; +} + +export function validateOpenClawExternalCodePluginPackageContents( + packageJson: unknown, + filePaths: Iterable, +): OpenClawExternalCodePluginValidation { + const validation = validateOpenClawExternalCodePluginPackageJson(packageJson); + const { root, openclaw } = readOpenClawBlock(packageJson); + const name = getTrimmedString(root?.name) ?? "package"; + const packageFiles = new Set(Array.from(filePaths, normalizePackagePath)); + const sourceEntries = getTrimmedStringArray(openclaw?.extensions); + const runtimeEntries = getTrimmedStringArray(openclaw?.runtimeExtensions); + + if (runtimeEntries.length > 0 && runtimeEntries.length !== sourceEntries.length) { + validation.issues.push({ + fieldPath: "openclaw.runtimeExtensions", + message: `${name} openclaw.runtimeExtensions length (${runtimeEntries.length}) must match openclaw.extensions length (${sourceEntries.length}).`, + }); + } + + for (const runtimeEntry of runtimeEntries) { + const normalized = normalizePackagePath(runtimeEntry); + if (!packageFiles.has(normalized)) { + validation.issues.push({ + fieldPath: "openclaw.runtimeExtensions", + message: `${name} runtime extension entry not found: ./${normalized}`, + }); + } + } + + if (runtimeEntries.length === 0) { + for (const sourceEntry of sourceEntries) { + if (!isTypeScriptRuntimeEntry(sourceEntry)) continue; + const candidates = compiledRuntimeCandidates(sourceEntry); + if (candidates.some((candidate) => packageFiles.has(candidate))) continue; + validation.issues.push({ + fieldPath: "openclaw.extensions", + message: `${name} requires compiled runtime output for TypeScript entry ${sourceEntry}: expected ${candidates.map((candidate) => `./${candidate}`).join(", ")}`, + }); + } + } + + return validation; +} diff --git a/dt-skill/src/schema/packages.ts b/dt-skill/src/schema/packages.ts new file mode 100644 index 00000000..b63cdcbb --- /dev/null +++ b/dt-skill/src/schema/packages.ts @@ -0,0 +1,751 @@ +import { type inferred, type } from "arktype"; +import { CliPublishFileSchema, PublishSourceSchema } from "./schemas.js"; + +export const PackageFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"'); +export type PackageFamily = (typeof PackageFamilySchema)[inferred]; + +export const PackageChannelSchema = type('"official"|"community"|"private"'); +export type PackageChannel = (typeof PackageChannelSchema)[inferred]; + +export const PackageVerificationTierSchema = type( + '"structural"|"source-linked"|"provenance-verified"|"rebuild-verified"', +); +export type PackageVerificationTier = (typeof PackageVerificationTierSchema)[inferred]; + +export const PackageVerificationScopeSchema = type('"artifact-only"|"dependency-graph-aware"'); +export type PackageVerificationScope = (typeof PackageVerificationScopeSchema)[inferred]; + +export const PackageCompatibilitySchema = type({ + pluginApiRange: "string?", + builtWithOpenClawVersion: "string?", + pluginSdkVersion: "string?", + minGatewayVersion: "string?", +}); +export type PackageCompatibility = (typeof PackageCompatibilitySchema)[inferred]; + +export const PackageCapabilitySummarySchema = type({ + executesCode: "boolean", + runtimeId: "string?", + pluginKind: "string?", + channels: "string[]?", + providers: "string[]?", + hooks: "string[]?", + bundledSkills: "string[]?", + setupEntry: "boolean?", + configSchema: "boolean?", + configUiHints: "boolean?", + materializesDependencies: "boolean?", + toolNames: "string[]?", + commandNames: "string[]?", + serviceNames: "string[]?", + capabilityTags: "string[]?", + httpRouteCount: "number?", + bundleFormat: "string?", + hostTargets: "string[]?", +}); +export type PackageCapabilitySummary = (typeof PackageCapabilitySummarySchema)[inferred]; + +export const PackageVerificationSummarySchema = type({ + tier: PackageVerificationTierSchema, + scope: PackageVerificationScopeSchema, + summary: "string?", + sourceRepo: "string?", + sourceCommit: "string?", + sourceTag: "string?", + hasProvenance: "boolean?", + scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', +}); +export type PackageVerificationSummary = (typeof PackageVerificationSummarySchema)[inferred]; + +export const PackageStatsSchema = type({ + downloads: "number", + installs: "number", + stars: "number", + versions: "number", +}); +export type PackageStats = (typeof PackageStatsSchema)[inferred]; + +export const PackageArtifactKindSchema = type('"legacy-zip"|"npm-pack"'); +export type PackageArtifactKind = (typeof PackageArtifactKindSchema)[inferred]; + +export const PackageReleaseModerationStateSchema = type('"approved"|"quarantined"|"revoked"'); +export type PackageReleaseModerationState = (typeof PackageReleaseModerationStateSchema)[inferred]; + +export const PackageReportStatusSchema = type('"open"|"confirmed"|"dismissed"'); +export type PackageReportStatus = (typeof PackageReportStatusSchema)[inferred]; +export const PackageReportFinalActionSchema = type('"none"|"quarantine"|"revoke"'); +export type PackageReportFinalAction = (typeof PackageReportFinalActionSchema)[inferred]; + +export const PackageReportListStatusSchema = PackageReportStatusSchema.or('"all"'); +export type PackageReportListStatus = (typeof PackageReportListStatusSchema)[inferred]; + +export const PackageAppealStatusSchema = type('"open"|"accepted"|"rejected"'); +export type PackageAppealStatus = (typeof PackageAppealStatusSchema)[inferred]; +export const PackageAppealFinalActionSchema = type('"none"|"approve"'); +export type PackageAppealFinalAction = (typeof PackageAppealFinalActionSchema)[inferred]; + +export const PackageAppealListStatusSchema = PackageAppealStatusSchema.or('"all"'); +export type PackageAppealListStatus = (typeof PackageAppealListStatusSchema)[inferred]; + +export const PackageOfficialMigrationPhaseSchema = type( + '"planned"|"published"|"clawpack-ready"|"legacy-zip-only"|"metadata-ready"|"blocked"|"ready-for-openclaw"', +); +export type PackageOfficialMigrationPhase = (typeof PackageOfficialMigrationPhaseSchema)[inferred]; + +export const PackageOfficialMigrationListPhaseSchema = + PackageOfficialMigrationPhaseSchema.or('"all"'); +export type PackageOfficialMigrationListPhase = + (typeof PackageOfficialMigrationListPhaseSchema)[inferred]; + +export const PackageArtifactSummarySchema = type({ + kind: PackageArtifactKindSchema, + sha256: "string?", + size: "number?", + format: "string?", + npmIntegrity: "string?", + npmShasum: "string?", + npmTarballName: "string?", + npmUnpackedSize: "number?", + npmFileCount: "number?", + source: '"clawhub"?', + artifactKind: PackageArtifactKindSchema.optional(), + artifactSha256: "string?", + packageName: "string?", + version: "string?", +}); +export type PackageArtifactSummary = (typeof PackageArtifactSummarySchema)[inferred]; + +export const PackagePublishArtifactSchema = type({ + kind: '"npm-pack"', + storageId: "string", + sha256: "string", + size: "number", + format: '"tgz"', + npmIntegrity: "string", + npmShasum: "string", + npmTarballName: "string", + npmUnpackedSize: "number", + npmFileCount: "number", +}); +export type PackagePublishArtifact = (typeof PackagePublishArtifactSchema)[inferred]; + +export const PackageVtAnalysisSchema = type({ + status: "string", + verdict: "string?", + analysis: "string?", + source: "string?", + checkedAt: "number", +}); +export type PackageVtAnalysis = (typeof PackageVtAnalysisSchema)[inferred]; + +export const PackageLlmAnalysisDimensionSchema = type({ + name: "string", + label: "string", + rating: "string", + detail: "string", +}); +export type PackageLlmAnalysisDimension = (typeof PackageLlmAnalysisDimensionSchema)[inferred]; + +export const PackageLlmAnalysisSchema = type({ + status: "string", + verdict: "string?", + confidence: "string?", + summary: "string?", + dimensions: PackageLlmAnalysisDimensionSchema.array().optional(), + guidance: "string?", + findings: "string?", + agenticRiskFindings: "unknown[]?", + riskSummary: "unknown?", + model: "string?", + checkedAt: "number", +}); +export type PackageLlmAnalysis = (typeof PackageLlmAnalysisSchema)[inferred]; + +export const PackageStaticFindingSchema = type({ + code: "string", + severity: "string", + file: "string", + line: "number", + message: "string", + evidence: "string", +}); +export type PackageStaticFinding = (typeof PackageStaticFindingSchema)[inferred]; + +export const PackageStaticScanSchema = type({ + status: "string", + reasonCodes: "string[]", + findings: PackageStaticFindingSchema.array(), + summary: "string", + engineVersion: "string", + checkedAt: "number", +}); +export type PackageStaticScan = (typeof PackageStaticScanSchema)[inferred]; + +export const BundlePublishMetadataSchema = type({ + id: "string?", + format: "string?", + hostTargets: "string[]?", +}); +export type BundlePublishMetadata = (typeof BundlePublishMetadataSchema)[inferred]; + +export const PackageTrustedPublisherSchema = type({ + provider: '"github-actions"', + repository: "string", + repositoryId: "string", + repositoryOwner: "string", + repositoryOwnerId: "string", + workflowFilename: "string", + environment: "string?", +}); +export type PackageTrustedPublisher = (typeof PackageTrustedPublisherSchema)[inferred]; + +export const PackagePublishRequestSchema = type({ + name: "string", + displayName: "string?", + ownerHandle: "string?", + family: PackageFamilySchema, + version: "string", + changelog: "string", + clawScanNote: "string?", + manualOverrideReason: "string?", + channel: PackageChannelSchema.optional(), + tags: "string[]?", + source: PublishSourceSchema.optional(), + bundle: BundlePublishMetadataSchema.optional(), + artifact: PackagePublishArtifactSchema.optional(), + files: CliPublishFileSchema.array(), +}); +export type PackagePublishRequest = (typeof PackagePublishRequestSchema)[inferred]; + +export const PackageListItemSchema = type({ + name: "string", + displayName: "string", + family: PackageFamilySchema, + runtimeId: "string|null?", + channel: PackageChannelSchema, + isOfficial: "boolean", + summary: "string|null?", + ownerHandle: "string|null?", + createdAt: "number", + updatedAt: "number", + latestVersion: "string|null?", + capabilityTags: "string[]?", + executesCode: "boolean?", + verificationTier: PackageVerificationTierSchema.or("null").optional(), +}); +export type PackageListItem = (typeof PackageListItemSchema)[inferred]; + +export const ApiV1PackageListResponseSchema = type({ + items: PackageListItemSchema.array(), + nextCursor: "string|null", +}); + +export const ApiV1PackageSearchResponseSchema = type({ + results: type({ + score: "number", + package: PackageListItemSchema, + }).array(), +}); + +export const ApiV1PackageResponseSchema = type({ + package: type({ + name: "string", + displayName: "string", + family: PackageFamilySchema, + runtimeId: "string|null?", + channel: PackageChannelSchema, + isOfficial: "boolean", + summary: "string|null?", + ownerHandle: "string|null?", + createdAt: "number", + updatedAt: "number", + latestVersion: "string|null?", + tags: "unknown", + compatibility: PackageCompatibilitySchema.or("null").optional(), + capabilities: PackageCapabilitySummarySchema.or("null").optional(), + verification: PackageVerificationSummarySchema.or("null").optional(), + artifact: PackageArtifactSummarySchema.or("null").optional(), + stats: PackageStatsSchema.optional(), + }).or("null"), + owner: type({ + handle: "string|null", + displayName: "string|null?", + image: "string|null?", + }).or("null"), +}); + +export const ApiV1PackageVersionListResponseSchema = type({ + items: type({ + version: "string", + createdAt: "number", + changelog: "string", + distTags: "string[]?", + }).array(), + nextCursor: "string|null", +}); + +export const ApiV1PackageVersionResponseSchema = type({ + package: type({ + name: "string", + displayName: "string", + family: PackageFamilySchema, + }).or("null"), + version: type({ + version: "string", + createdAt: "number", + changelog: "string", + distTags: "string[]?", + files: "unknown", + compatibility: PackageCompatibilitySchema.or("null").optional(), + capabilities: PackageCapabilitySummarySchema.or("null").optional(), + verification: PackageVerificationSummarySchema.or("null").optional(), + artifact: PackageArtifactSummarySchema.or("null").optional(), + sha256hash: "string|null?", + vtAnalysis: PackageVtAnalysisSchema.or("null").optional(), + llmAnalysis: PackageLlmAnalysisSchema.or("null").optional(), + clawScanNote: "string|null?", + clawScanNoteUpdatedAt: "number|null?", + staticScan: PackageStaticScanSchema.or("null").optional(), + }).or("null"), +}); + +export const ApiV1PackageArtifactResponseSchema = type({ + package: type({ + name: "string", + displayName: "string", + family: PackageFamilySchema, + }), + version: "string", + artifact: type({ + kind: PackageArtifactKindSchema, + sha256: "string?", + size: "number?", + format: "string?", + npmIntegrity: "string?", + npmShasum: "string?", + npmTarballName: "string?", + npmUnpackedSize: "number?", + npmFileCount: "number?", + downloadUrl: "string", + tarballUrl: "string?", + legacyDownloadUrl: "string?", + source: '"clawhub"?', + artifactKind: PackageArtifactKindSchema.optional(), + artifactSha256: "string?", + packageName: "string?", + version: "string?", + }), +}); +export type ApiV1PackageArtifactResponse = (typeof ApiV1PackageArtifactResponseSchema)[inferred]; + +export const ApiV1PackageSecurityResponseSchema = type({ + package: type({ + name: "string", + displayName: "string", + family: PackageFamilySchema, + }), + release: type({ + releaseId: "string", + version: "string", + artifactKind: PackageArtifactKindSchema.or("null").optional(), + artifactSha256: "string?", + npmIntegrity: "string?", + npmShasum: "string?", + npmTarballName: "string?", + createdAt: "number", + }), + trust: type({ + scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', + moderationState: PackageReleaseModerationStateSchema.or("null").optional(), + blockedFromDownload: "boolean", + reasons: "string[]", + pending: "boolean", + stale: "boolean", + }), +}); +export type ApiV1PackageSecurityResponse = (typeof ApiV1PackageSecurityResponseSchema)[inferred]; + +export const PackageReleaseModerationRequestSchema = type({ + state: PackageReleaseModerationStateSchema, + reason: "string", +}); +export type PackageReleaseModerationRequest = + (typeof PackageReleaseModerationRequestSchema)[inferred]; + +export const PackageReportRequestSchema = type({ + reason: "string", + version: "string?", +}); +export type PackageReportRequest = (typeof PackageReportRequestSchema)[inferred]; + +export const ApiV1PackageReportResponseSchema = type({ + ok: "true", + reported: "boolean", + alreadyReported: "boolean", + packageId: "string", + releaseId: "string|null", + reportCount: "number", +}); +export type ApiV1PackageReportResponse = (typeof ApiV1PackageReportResponseSchema)[inferred]; + +export const PackageReportTriageRequestSchema = type({ + status: PackageReportStatusSchema, + note: "string?", + finalAction: PackageReportFinalActionSchema.optional(), +}); +export type PackageReportTriageRequest = (typeof PackageReportTriageRequestSchema)[inferred]; + +export const PackageAppealRequestSchema = type({ + version: "string", + message: "string", +}); +export type PackageAppealRequest = (typeof PackageAppealRequestSchema)[inferred]; + +export const ApiV1PackageAppealResponseSchema = type({ + ok: "true", + submitted: "boolean", + alreadyOpen: "boolean", + appealId: "string", + packageId: "string", + releaseId: "string", + status: PackageAppealStatusSchema, +}); +export type ApiV1PackageAppealResponse = (typeof ApiV1PackageAppealResponseSchema)[inferred]; + +export const PackageAppealResolveRequestSchema = type({ + status: PackageAppealStatusSchema, + note: "string?", + finalAction: PackageAppealFinalActionSchema.optional(), +}); +export type PackageAppealResolveRequest = (typeof PackageAppealResolveRequestSchema)[inferred]; + +export const ApiV1PackageAppealListResponseSchema = type({ + items: type({ + appealId: "string", + packageId: "string", + releaseId: "string", + name: "string", + displayName: "string", + family: PackageFamilySchema, + version: "string", + message: "string", + status: PackageAppealStatusSchema, + createdAt: "number", + submitter: type({ + userId: "string", + handle: "string|null?", + displayName: "string|null?", + }), + resolvedAt: "number|null?", + resolvedBy: "string|null?", + resolutionNote: "string|null?", + actionTaken: PackageAppealFinalActionSchema.or("null").optional(), + }).array(), + nextCursor: "string|null", + done: "boolean", +}); +export type ApiV1PackageAppealListResponse = + (typeof ApiV1PackageAppealListResponseSchema)[inferred]; + +export const ApiV1PackageAppealResolveResponseSchema = type({ + ok: "true", + appealId: "string", + packageId: "string", + releaseId: "string", + status: PackageAppealStatusSchema, + actionTaken: PackageAppealFinalActionSchema.optional(), +}); +export type ApiV1PackageAppealResolveResponse = + (typeof ApiV1PackageAppealResolveResponseSchema)[inferred]; + +export const ApiV1PackageReportListResponseSchema = type({ + items: type({ + reportId: "string", + packageId: "string", + releaseId: "string|null?", + name: "string", + displayName: "string", + family: PackageFamilySchema, + version: "string|null?", + reason: "string|null?", + status: PackageReportStatusSchema, + createdAt: "number", + reporter: type({ + userId: "string", + handle: "string|null?", + displayName: "string|null?", + }), + triagedAt: "number|null?", + triagedBy: "string|null?", + triageNote: "string|null?", + actionTaken: PackageReportFinalActionSchema.or("null").optional(), + }).array(), + nextCursor: "string|null", + done: "boolean", +}); +export type ApiV1PackageReportListResponse = + (typeof ApiV1PackageReportListResponseSchema)[inferred]; + +export const ApiV1PackageReportTriageResponseSchema = type({ + ok: "true", + reportId: "string", + packageId: "string", + status: PackageReportStatusSchema, + reportCount: "number", + actionTaken: PackageReportFinalActionSchema.optional(), +}); +export type ApiV1PackageReportTriageResponse = + (typeof ApiV1PackageReportTriageResponseSchema)[inferred]; + +export const ApiV1PackageModerationStatusResponseSchema = type({ + package: type({ + packageId: "string", + name: "string", + displayName: "string", + family: PackageFamilySchema, + channel: PackageChannelSchema, + isOfficial: "boolean", + reportCount: "number", + lastReportedAt: "number|null?", + scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', + }), + latestRelease: type({ + releaseId: "string", + version: "string", + artifactKind: PackageArtifactKindSchema.or("null").optional(), + scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', + moderationState: PackageReleaseModerationStateSchema.or("null").optional(), + moderationReason: "string|null?", + blockedFromDownload: "boolean", + reasons: "string[]", + createdAt: "number", + }).or("null"), +}); +export type ApiV1PackageModerationStatusResponse = + (typeof ApiV1PackageModerationStatusResponseSchema)[inferred]; + +export const PackageArtifactBackfillRequestSchema = type({ + cursor: "string|null?", + batchSize: "number?", + dryRun: "boolean?", +}); +export type PackageArtifactBackfillRequest = + (typeof PackageArtifactBackfillRequestSchema)[inferred]; + +export const ApiV1PackageArtifactBackfillResponseSchema = type({ + ok: "true", + scanned: "number", + updated: "number", + nextCursor: "string|null", + done: "boolean", + dryRun: "boolean", +}); +export type ApiV1PackageArtifactBackfillResponse = + (typeof ApiV1PackageArtifactBackfillResponseSchema)[inferred]; + +export const PackageReadinessCheckSchema = type({ + id: "string", + label: "string", + status: '"pass"|"warn"|"fail"', + message: "string", +}); +export type PackageReadinessCheck = (typeof PackageReadinessCheckSchema)[inferred]; + +export const ApiV1PackageReadinessResponseSchema = type({ + package: type({ + name: "string", + displayName: "string", + family: PackageFamilySchema, + isOfficial: "boolean", + latestVersion: "string|null?", + }), + ready: "boolean", + checks: PackageReadinessCheckSchema.array(), + blockers: "string[]", +}); +export type ApiV1PackageReadinessResponse = (typeof ApiV1PackageReadinessResponseSchema)[inferred]; + +export const PackageTransferRequestSchema = type({ + toOwner: "string", + reason: "string?", +}); +export type PackageTransferRequest = (typeof PackageTransferRequestSchema)[inferred]; + +export const ApiV1PackageTransferResponseSchema = type({ + ok: "true", + packageId: "string", + name: "string", + ownerUserId: "string", + ownerPublisherId: "string?", + channel: PackageChannelSchema, + isOfficial: "boolean", +}); +export type ApiV1PackageTransferResponse = (typeof ApiV1PackageTransferResponseSchema)[inferred]; + +export const PackageRepairNameRequestSchema = type({ + nextName: "string", + retireTarget: "boolean?", + owner: "string?", + reason: "string", + dryRun: "boolean?", +}); +export type PackageRepairNameRequest = (typeof PackageRepairNameRequestSchema)[inferred]; + +export const PackageRepairNamePackageSchema = type({ + packageId: "string", + name: "string", + runtimeId: "string|null?", + ownerUserId: "string", + ownerPublisherId: "string|null?", + channel: PackageChannelSchema, + softDeletedAt: "number|null?", +}); +export type PackageRepairNamePackage = (typeof PackageRepairNamePackageSchema)[inferred]; + +export const PackageRepairNameOperationSchema = type({ + action: '"retire-target"|"rename-source"|"transfer-owner"', + packageId: "string?", + from: "string?", + to: "string?", + owner: "string?", +}); +export type PackageRepairNameOperation = (typeof PackageRepairNameOperationSchema)[inferred]; + +export const ApiV1PackageRepairNameResponseSchema = type({ + ok: "true", + dryRun: "boolean", + source: PackageRepairNamePackageSchema, + target: PackageRepairNamePackageSchema.or("null"), + retiredName: "string|null?", + operations: PackageRepairNameOperationSchema.array(), +}); +export type ApiV1PackageRepairNameResponse = + (typeof ApiV1PackageRepairNameResponseSchema)[inferred]; + +export const PackageOfficialMigrationUpsertRequestSchema = type({ + bundledPluginId: "string", + packageName: "string", + owner: "string?", + sourceRepo: "string?", + sourcePath: "string?", + sourceCommit: "string?", + phase: PackageOfficialMigrationPhaseSchema.optional(), + blockers: "string[]?", + hostTargetsComplete: "boolean?", + scanClean: "boolean?", + moderationApproved: "boolean?", + runtimeBundlesReady: "boolean?", + notes: "string?", +}); +export type PackageOfficialMigrationUpsertRequest = + (typeof PackageOfficialMigrationUpsertRequestSchema)[inferred]; + +export const PackageOfficialMigrationItemSchema = type({ + migrationId: "string", + bundledPluginId: "string", + packageName: "string", + packageId: "string|null?", + owner: "string|null?", + sourceRepo: "string|null?", + sourcePath: "string|null?", + sourceCommit: "string|null?", + phase: PackageOfficialMigrationPhaseSchema, + blockers: "string[]", + hostTargetsComplete: "boolean", + scanClean: "boolean", + moderationApproved: "boolean", + runtimeBundlesReady: "boolean", + notes: "string|null?", + createdAt: "number", + updatedAt: "number", +}); +export type PackageOfficialMigrationItem = (typeof PackageOfficialMigrationItemSchema)[inferred]; + +export const ApiV1PackageOfficialMigrationListResponseSchema = type({ + items: PackageOfficialMigrationItemSchema.array(), + nextCursor: "string|null", + done: "boolean", +}); +export type ApiV1PackageOfficialMigrationListResponse = + (typeof ApiV1PackageOfficialMigrationListResponseSchema)[inferred]; + +export const ApiV1PackageOfficialMigrationResponseSchema = type({ + ok: "true", + migration: PackageOfficialMigrationItemSchema, +}); +export type ApiV1PackageOfficialMigrationResponse = + (typeof ApiV1PackageOfficialMigrationResponseSchema)[inferred]; + +export const PackageModerationQueueStatusSchema = type('"open"|"blocked"|"manual"|"all"'); +export type PackageModerationQueueStatus = (typeof PackageModerationQueueStatusSchema)[inferred]; + +export const ApiV1PackageModerationQueueResponseSchema = type({ + items: type({ + packageId: "string", + releaseId: "string", + name: "string", + displayName: "string", + family: PackageFamilySchema, + channel: PackageChannelSchema, + isOfficial: "boolean", + version: "string", + createdAt: "number", + artifactKind: PackageArtifactKindSchema.or("null").optional(), + scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', + moderationState: PackageReleaseModerationStateSchema.or("null").optional(), + moderationReason: "string|null?", + sourceRepo: "string|null?", + sourceCommit: "string|null?", + reportCount: "number", + lastReportedAt: "number|null?", + reasons: "string[]", + }).array(), + nextCursor: "string|null", + done: "boolean", +}); +export type ApiV1PackageModerationQueueResponse = + (typeof ApiV1PackageModerationQueueResponseSchema)[inferred]; + +export const ApiV1PackageReleaseModerationResponseSchema = type({ + ok: "true", + packageId: "string", + releaseId: "string", + state: PackageReleaseModerationStateSchema, + scanStatus: '"clean"|"malicious"', +}); +export type ApiV1PackageReleaseModerationResponse = + (typeof ApiV1PackageReleaseModerationResponseSchema)[inferred]; + +export const ApiV1PackagePublishResponseSchema = type({ + ok: "true", + packageId: "string", + releaseId: "string", +}); +export type ApiV1PackagePublishResponse = (typeof ApiV1PackagePublishResponseSchema)[inferred]; + +export const PackageTrustedPublisherUpsertRequestSchema = type({ + repository: "string", + workflowFilename: "string", + environment: "string?", +}); +export type PackageTrustedPublisherUpsertRequest = + (typeof PackageTrustedPublisherUpsertRequestSchema)[inferred]; + +export const ApiV1PackageTrustedPublisherResponseSchema = type({ + trustedPublisher: PackageTrustedPublisherSchema.or("null"), +}); +export type ApiV1PackageTrustedPublisherResponse = + (typeof ApiV1PackageTrustedPublisherResponseSchema)[inferred]; + +export const PublishTokenMintRequestSchema = type({ + packageName: "string", + version: "string", + githubOidcToken: "string", +}); +export type PublishTokenMintRequest = (typeof PublishTokenMintRequestSchema)[inferred]; + +export const ApiV1PublishTokenMintResponseSchema = type({ + token: "string", + expiresAt: "number", +}); +export type ApiV1PublishTokenMintResponse = (typeof ApiV1PublishTokenMintResponseSchema)[inferred]; diff --git a/dt-skill/src/schema/routes.ts b/dt-skill/src/schema/routes.ts new file mode 100644 index 00000000..fcc40f36 --- /dev/null +++ b/dt-skill/src/schema/routes.ts @@ -0,0 +1,29 @@ +export const LegacyApiRoutes = { + download: "/api/download", + search: "/api/search", + skill: "/api/skill", + skillResolve: "/api/skill/resolve", + cliWhoami: "/api/cli/whoami", + cliUploadUrl: "/api/cli/upload-url", + cliPublish: "/api/cli/publish", + cliTelemetrySync: "/api/cli/telemetry/sync", + cliSkillDelete: "/api/cli/skill/delete", + cliSkillUndelete: "/api/cli/skill/undelete", +} as const; + +export const ApiRoutes = { + search: "/api/v1/search", + resolve: "/api/v1/resolve", + download: "/api/v1/download", + publishTokenMint: "/api/v1/publish/token/mint", + skills: "/api/v1/skills", + packages: "/api/v1/packages", + codePlugins: "/api/v1/code-plugins", + bundlePlugins: "/api/v1/bundle-plugins", + stars: "/api/v1/stars", + transfers: "/api/v1/transfers", + publishers: "/api/v1/publishers", + souls: "/api/v1/souls", + users: "/api/v1/users", + whoami: "/api/v1/whoami", +} as const; diff --git a/dt-skill/src/schema/schemas.test.ts b/dt-skill/src/schema/schemas.test.ts new file mode 100644 index 00000000..67f4af63 --- /dev/null +++ b/dt-skill/src/schema/schemas.test.ts @@ -0,0 +1,53 @@ +/* @vitest-environment node */ + +import { describe, expect, it } from "vitest"; +import { parseArk } from "./ark"; +import { ApiV1SearchResponseSchema, ClawdisSkillMetadataSchema } from "./schemas"; + +describe("packages/clawhub skill metadata schema", () => { + it("preserves optional env var declarations", () => { + const parsed = parseArk( + ClawdisSkillMetadataSchema, + { + envVars: [ + { name: "TODOIST_API_KEY", required: true, description: "API token" }, + { name: "TODOIST_PROJECT_ID", required: false, description: "Default project" }, + ], + }, + "Skill metadata", + ); + + expect(parsed.envVars?.[1]).toEqual({ + name: "TODOIST_PROJECT_ID", + required: false, + description: "Default project", + }); + }); + + it("parses v1 search owner metadata", () => { + const parsed = parseArk( + ApiV1SearchResponseSchema, + { + results: [ + { + slug: "demo", + displayName: "Demo", + summary: null, + version: "1.0.0", + score: 1, + ownerHandle: "openclaw", + owner: { + handle: "openclaw", + displayName: "OpenClaw", + image: null, + }, + }, + ], + }, + "Search", + ); + + expect(parsed.results[0]?.ownerHandle).toBe("openclaw"); + expect(parsed.results[0]?.owner?.displayName).toBe("OpenClaw"); + }); +}); diff --git a/dt-skill/src/schema/schemas.ts b/dt-skill/src/schema/schemas.ts new file mode 100644 index 00000000..29282c4d --- /dev/null +++ b/dt-skill/src/schema/schemas.ts @@ -0,0 +1,592 @@ +import { type inferred, type } from "arktype"; + +export const GlobalConfigSchema = type({ + registry: "string", + token: "string?", +}); +export type GlobalConfig = (typeof GlobalConfigSchema)[inferred]; + +export const WellKnownConfigSchema = type({ + apiBase: "string", + authBase: "string?", + minCliVersion: "string?", +}).or({ + registry: "string", + authBase: "string?", + minCliVersion: "string?", +}); +export type WellKnownConfig = (typeof WellKnownConfigSchema)[inferred]; + +export const LockfileSchema = type({ + version: "1", + skills: { + "[string]": { + version: "string|null", + installedAt: "number", + pinned: "boolean?", + pinReason: "string?", + }, + }, +}); +export type Lockfile = (typeof LockfileSchema)[inferred]; + +export const ApiCliWhoamiResponseSchema = type({ + user: { + handle: "string|null", + }, +}); + +export const ApiSearchResponseSchema = type({ + results: type({ + slug: "string?", + displayName: "string?", + version: "string|null?", + score: "number", + }).array(), +}); + +export const ApiSkillMetaResponseSchema = type({ + latestVersion: type({ + version: "string", + }).optional(), + skill: "unknown|null?", +}); + +export const ApiCliUploadUrlResponseSchema = type({ + uploadUrl: "string", +}); + +export const ApiUploadFileResponseSchema = type({ + storageId: "string", +}); + +export const CliPublishFileSchema = type({ + path: "string", + size: "number", + storageId: "string", + sha256: "string", + contentType: "string?", +}); +export type CliPublishFile = (typeof CliPublishFileSchema)[inferred]; + +export const PublishSourceSchema = type({ + kind: '"github"', + url: "string", + repo: "string", + ref: "string", + commit: "string", + path: "string", + importedAt: "number", +}); + +export const CliPublishRequestSchema = type({ + slug: "string", + displayName: "string", + ownerHandle: "string?", + migrateOwner: "boolean?", + version: "string", + changelog: "string", + clawScanNote: "string?", + acceptLicenseTerms: "boolean?", + tags: "string[]?", + source: PublishSourceSchema.optional(), + forkOf: type({ + slug: "string", + version: "string?", + }).optional(), + files: CliPublishFileSchema.array(), +}); +export type CliPublishRequest = (typeof CliPublishRequestSchema)[inferred]; + +export const ApiCliPublishResponseSchema = type({ + ok: "true", + skillId: "string", + versionId: "string", +}); + +export const CliSkillDeleteRequestSchema = type({ + slug: "string", + reason: "string?", +}); +export type CliSkillDeleteRequest = (typeof CliSkillDeleteRequestSchema)[inferred]; + +export const ApiCliSkillDeleteResponseSchema = type({ + ok: "true", + slugReservedUntil: "number?", +}); + +export const ApiSkillResolveResponseSchema = type({ + match: type({ version: "string" }).or("null"), + latestVersion: type({ version: "string" }).or("null"), +}); + +export const CliTelemetrySyncRequestSchema = type({ + roots: type({ + rootId: "string", + label: "string", + skills: type({ + slug: "string", + version: "string|null?", + }).array(), + }).array(), +}); +export type CliTelemetrySyncRequest = (typeof CliTelemetrySyncRequestSchema)[inferred]; + +export const ApiCliTelemetrySyncResponseSchema = type({ + ok: "true", +}); + +export const ApiV1WhoamiResponseSchema = type({ + user: { + handle: "string|null", + displayName: "string|null?", + image: "string|null?", + role: '"admin"|"moderator"|"user"|null?', + }, +}); + +export const ApiV1UserSearchResponseSchema = type({ + items: type({ + userId: "string", + handle: "string|null", + displayName: "string|null?", + name: "string|null?", + role: '"admin"|"moderator"|"user"|null?', + }).array(), + total: "number", +}); + +export const ApiV1PublisherCreateResponseSchema = type({ + ok: "true", + publisherId: "string", + handle: "string", + created: "true", + trusted: "false", +}); +export type ApiV1PublisherCreateResponse = (typeof ApiV1PublisherCreateResponseSchema)[inferred]; + +export const ApiV1SearchResponseSchema = type({ + results: type({ + slug: "string?", + displayName: "string?", + summary: "string|null?", + version: "string|null?", + score: "number", + updatedAt: "number?", + ownerHandle: "string|null?", + owner: type({ + handle: "string|null?", + displayName: "string|null?", + image: "string|null?", + }) + .or("null") + .optional(), + }).array(), +}); +export type ApiV1SearchResponse = (typeof ApiV1SearchResponseSchema)[inferred]; + +export const ApiV1SkillListResponseSchema = type({ + items: type({ + slug: "string", + displayName: "string", + summary: "string|null?", + tags: "unknown", + stats: "unknown", + createdAt: "number", + updatedAt: "number", + latestVersion: type({ + version: "string", + createdAt: "number", + changelog: "string", + license: '"MIT-0"|null?', + }).optional(), + }).array(), + nextCursor: "string|null", +}); +export type ApiV1SkillListResponse = (typeof ApiV1SkillListResponseSchema)[inferred]; + +export const ApiV1SkillResponseSchema = type({ + skill: type({ + slug: "string", + displayName: "string", + summary: "string|null?", + tags: "unknown", + stats: "unknown", + createdAt: "number", + updatedAt: "number", + isPackage: "boolean?", + parentSlug: "string|null?", + children: "unknown[]?", + }).or("null"), + latestVersion: type({ + version: "string", + createdAt: "number", + changelog: "string", + license: '"MIT-0"|null?', + }).or("null"), + owner: type({ + handle: "string|null", + displayName: "string|null?", + image: "string|null?", + }).or("null"), + moderation: type({ + isSuspicious: "boolean", + isMalwareBlocked: "boolean", + verdict: '"clean"|"suspicious"|"malicious"?', + reasonCodes: "string[]?", + updatedAt: "number|null?", + engineVersion: "string|null?", + summary: "string|null?", + }) + .or("null") + .optional(), +}); +export type ApiV1SkillResponse = (typeof ApiV1SkillResponseSchema)[inferred]; + +export const ApiV1SkillModerationResponseSchema = type({ + moderation: type({ + isSuspicious: "boolean", + isMalwareBlocked: "boolean", + verdict: '"clean"|"suspicious"|"malicious"', + reasonCodes: "string[]", + updatedAt: "number|null?", + engineVersion: "string|null?", + summary: "string|null?", + legacyReason: "string|null?", + evidence: type({ + code: "string", + severity: '"info"|"warn"|"critical"', + file: "string", + line: "number", + message: "string", + evidence: "string", + }).array(), + }).or("null"), +}); + +export const SkillReportStatusSchema = type('"open"|"confirmed"|"dismissed"'); +export type SkillReportStatus = (typeof SkillReportStatusSchema)[inferred]; +export const SkillReportFinalActionSchema = type('"none"|"hide"'); +export type SkillReportFinalAction = (typeof SkillReportFinalActionSchema)[inferred]; + +export const SkillReportListStatusSchema = SkillReportStatusSchema.or('"all"'); +export type SkillReportListStatus = (typeof SkillReportListStatusSchema)[inferred]; + +export const SkillAppealStatusSchema = type('"open"|"accepted"|"rejected"'); +export type SkillAppealStatus = (typeof SkillAppealStatusSchema)[inferred]; +export const SkillAppealFinalActionSchema = type('"none"|"restore"'); +export type SkillAppealFinalAction = (typeof SkillAppealFinalActionSchema)[inferred]; + +export const SkillAppealListStatusSchema = SkillAppealStatusSchema.or('"all"'); +export type SkillAppealListStatus = (typeof SkillAppealListStatusSchema)[inferred]; + +export const SkillAppealRequestSchema = type({ + version: "string?", + message: "string", +}); +export type SkillAppealRequest = (typeof SkillAppealRequestSchema)[inferred]; + +export const ApiV1SkillReportResponseSchema = type({ + ok: "true", + reported: "boolean", + alreadyReported: "boolean", + reportId: "string", + skillId: "string", + reportCount: "number", +}); +export type ApiV1SkillReportResponse = (typeof ApiV1SkillReportResponseSchema)[inferred]; + +export const ApiV1SkillAppealResponseSchema = type({ + ok: "true", + submitted: "boolean", + alreadyOpen: "boolean", + appealId: "string", + skillId: "string", + status: SkillAppealStatusSchema, +}); +export type ApiV1SkillAppealResponse = (typeof ApiV1SkillAppealResponseSchema)[inferred]; + +export const SkillReportTriageRequestSchema = type({ + status: SkillReportStatusSchema, + note: "string?", + finalAction: SkillReportFinalActionSchema.optional(), +}); +export type SkillReportTriageRequest = (typeof SkillReportTriageRequestSchema)[inferred]; + +export const SkillAppealResolveRequestSchema = type({ + status: SkillAppealStatusSchema, + note: "string?", + finalAction: SkillAppealFinalActionSchema.optional(), +}); +export type SkillAppealResolveRequest = (typeof SkillAppealResolveRequestSchema)[inferred]; + +export const ApiV1SkillReportListResponseSchema = type({ + items: type({ + reportId: "string", + skillId: "string", + skillVersionId: "string|null?", + slug: "string", + displayName: "string", + version: "string|null?", + reason: "string|null?", + status: SkillReportStatusSchema, + createdAt: "number", + reporter: type({ + userId: "string", + handle: "string|null?", + displayName: "string|null?", + }), + triagedAt: "number|null?", + triagedBy: "string|null?", + triageNote: "string|null?", + actionTaken: SkillReportFinalActionSchema.or("null").optional(), + }).array(), + nextCursor: "string|null", + done: "boolean", +}); +export type ApiV1SkillReportListResponse = (typeof ApiV1SkillReportListResponseSchema)[inferred]; + +export const ApiV1SkillReportTriageResponseSchema = type({ + ok: "true", + reportId: "string", + skillId: "string", + status: SkillReportStatusSchema, + reportCount: "number", + actionTaken: SkillReportFinalActionSchema.optional(), +}); +export type ApiV1SkillReportTriageResponse = + (typeof ApiV1SkillReportTriageResponseSchema)[inferred]; + +export const ApiV1SkillAppealListResponseSchema = type({ + items: type({ + appealId: "string", + skillId: "string", + skillVersionId: "string|null?", + slug: "string", + displayName: "string", + version: "string|null?", + message: "string", + status: SkillAppealStatusSchema, + createdAt: "number", + submitter: type({ + userId: "string", + handle: "string|null?", + displayName: "string|null?", + }), + resolvedAt: "number|null?", + resolvedBy: "string|null?", + resolutionNote: "string|null?", + actionTaken: SkillAppealFinalActionSchema.or("null").optional(), + }).array(), + nextCursor: "string|null", + done: "boolean", +}); +export type ApiV1SkillAppealListResponse = (typeof ApiV1SkillAppealListResponseSchema)[inferred]; + +export const ApiV1SkillAppealResolveResponseSchema = type({ + ok: "true", + appealId: "string", + skillId: "string", + status: SkillAppealStatusSchema, + actionTaken: SkillAppealFinalActionSchema.optional(), +}); +export type ApiV1SkillAppealResolveResponse = + (typeof ApiV1SkillAppealResolveResponseSchema)[inferred]; + +export const ApiV1SkillRescanResponseSchema = type({ + ok: "true", + slug: "string", + version: "string", + skillId: "string", + skillVersionId: "string", + jobId: "string", + alreadyQueued: "boolean", +}); +export type ApiV1SkillRescanResponse = (typeof ApiV1SkillRescanResponseSchema)[inferred]; + +export const ApiV1SkillVersionListResponseSchema = type({ + items: type({ + version: "string", + createdAt: "number", + changelog: "string", + changelogSource: '"auto"|"user"|null?', + }).array(), + nextCursor: "string|null", +}); + +export const ApiV1SkillVersionResponseSchema = type({ + version: type({ + version: "string", + createdAt: "number", + changelog: "string", + changelogSource: '"auto"|"user"|null?', + license: '"MIT-0"|null?', + files: "unknown?", + }).or("null"), + skill: type({ + slug: "string", + displayName: "string", + }).or("null"), +}); +export type ApiV1SkillVersionResponse = (typeof ApiV1SkillVersionResponseSchema)[inferred]; + +export const ApiV1SkillResolveResponseSchema = type({ + match: type({ version: "string" }).or("null"), + latestVersion: type({ version: "string" }).or("null"), +}); +export type ApiV1SkillResolveResponse = (typeof ApiV1SkillResolveResponseSchema)[inferred]; + +export const ApiV1PublishResponseSchema = type({ + ok: "true", + skillId: "string", + versionId: "string", +}); + +export const ApiV1DeleteResponseSchema = type({ + ok: "true", + slugReservedUntil: "number?", +}); + +export const ApiV1SkillRenameResponseSchema = type({ + ok: "true", + slug: "string", + previousSlug: "string", +}); + +export const ApiV1SkillMergeResponseSchema = type({ + ok: "true", + sourceSlug: "string", + targetSlug: "string", +}); + +export const ApiV1TransferRequestResponseSchema = type({ + ok: "true", + transferId: "string?", + toUserHandle: "string?", + toPublisherHandle: "string?", + skillSlug: "string?", + expiresAt: "number?", + transferred: "boolean?", +}); + +export const ApiV1TransferDecisionResponseSchema = type({ + ok: "true", + skillSlug: "string?", +}); + +export const ApiV1TransferListResponseSchema = type({ + transfers: type({ + _id: "string", + skill: type({ + _id: "string", + slug: "string", + displayName: "string", + }), + fromUser: type({ + _id: "string", + handle: "string|null", + displayName: "string|null", + }).optional(), + toUser: type({ + _id: "string", + handle: "string|null", + displayName: "string|null", + }).optional(), + message: "string?", + requestedAt: "number", + expiresAt: "number", + }).array(), +}); + +export const ApiV1BanUserResponseSchema = type({ + ok: "true", + alreadyBanned: "boolean", + deletedSkills: "number", +}); + +export const ApiV1UnbanUserResponseSchema = type({ + ok: "true", + alreadyUnbanned: "boolean", + restoredSkills: "number?", +}); + +export const ApiV1ReclassifyBanResponseSchema = type({ + ok: "true", + dryRun: "boolean", + userId: "string", + handle: "string|null", + previousReason: "string|null", + nextReason: "string", + changed: "boolean", +}); + +export const ApiV1RemediateAutobansResponseSchema = type({ + ok: "true", + dryRun: "boolean", + scanned: "number", + wouldUnban: "number", + unbanned: "number", + skipped: "number", + restoredSkills: "number", + restoredPackages: "number", + items: "unknown[]", + "nextCursor?": "string|null", + "done?": "boolean", +}); + +export const ApiV1SetRoleResponseSchema = type({ + ok: "true", + role: '"admin"|"moderator"|"user"', +}); + +export const ApiV1StarResponseSchema = type({ + ok: "true", + starred: "boolean", + alreadyStarred: "boolean", +}); + +export const ApiV1UnstarResponseSchema = type({ + ok: "true", + unstarred: "boolean", + alreadyUnstarred: "boolean", +}); + +export const SkillInstallSpecSchema = type({ + id: "string?", + kind: '"brew"|"node"|"go"|"uv"', + label: "string?", + bins: "string[]?", + formula: "string?", + tap: "string?", + package: "string?", + module: "string?", +}); +export type SkillInstallSpec = (typeof SkillInstallSpecSchema)[inferred]; + +export const ClawdisRequiresSchema = type({ + bins: "string[]?", + anyBins: "string[]?", + env: "string[]?", + config: "string[]?", +}); +export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]; + +export const EnvVarDeclarationSchema = type({ + name: "string", + required: "boolean?", + description: "string?", +}); +export type EnvVarDeclaration = (typeof EnvVarDeclarationSchema)[inferred]; + +export const ClawdisSkillMetadataSchema = type({ + always: "boolean?", + skillKey: "string?", + primaryEnv: "string?", + emoji: "string?", + homepage: "string?", + os: "string[]?", + requires: ClawdisRequiresSchema.optional(), + install: SkillInstallSpecSchema.array().optional(), + envVars: EnvVarDeclarationSchema.array().optional(), +}); +export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]; diff --git a/dt-skill/src/schema/skillFingerprintContract.test.ts b/dt-skill/src/schema/skillFingerprintContract.test.ts new file mode 100644 index 00000000..9a334fe1 --- /dev/null +++ b/dt-skill/src/schema/skillFingerprintContract.test.ts @@ -0,0 +1,36 @@ +/* @vitest-environment node */ + +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { buildSkillFingerprint, fingerprintFromGoldenCase, sha256Hex } from "./skillFingerprintContract.js"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const goldenVectors = JSON.parse( + readFileSync(resolve(repoRoot, "contracts/skill-fingerprint/golden-vectors.v1.json"), "utf8"), +); + +describe("skill fingerprint contract adapter", () => { + it("matches shared golden vectors", () => { + for (const testCase of goldenVectors.cases) { + const fingerprint = fingerprintFromGoldenCase(testCase); + if (testCase.fingerprint) { + expect(fingerprint, testCase.name).toBe(testCase.fingerprint); + } + expect(fingerprintFromGoldenCase(testCase), testCase.name).toBe(fingerprint); + } + }); + + it("sorts paths before hashing", () => { + const fingerprint = buildSkillFingerprint([ + { path: "b.txt", sha256: sha256Hex("b") }, + { path: "a.txt", sha256: sha256Hex("a") }, + ]); + const expected = buildSkillFingerprint([ + { path: "a.txt", sha256: sha256Hex("a") }, + { path: "b.txt", sha256: sha256Hex("b") }, + ]); + expect(fingerprint).toBe(expected); + }); +}); diff --git a/dt-skill/src/schema/skillFingerprintContract.ts b/dt-skill/src/schema/skillFingerprintContract.ts new file mode 100644 index 00000000..a02a0b99 --- /dev/null +++ b/dt-skill/src/schema/skillFingerprintContract.ts @@ -0,0 +1,51 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +const contract = require("../../../contracts/skill-fingerprint/index.js"); + +export const TEXT_FILE_EXTENSIONS = contract.TEXT_FILE_EXTENSIONS as readonly string[]; +export const TEXT_FILE_EXTENSION_SET = contract.TEXT_FILE_EXTENSION_SET as ReadonlySet; +export const FINGERPRINT_IGNORE_FILENAMES = + contract.FINGERPRINT_IGNORE_FILENAMES as readonly string[]; +export const TEXT_SAMPLE_BYTES = contract.TEXT_SAMPLE_BYTES as number; + +export const normalizeFilePath = contract.normalizeFilePath as (filePath: string) => string; +export const getFileExtension = contract.getFileExtension as (filePath: string) => string; +export const hasDotPathSegment = contract.hasDotPathSegment as (filePath: string) => boolean; +export const isLikelyTextBytes = contract.isLikelyTextBytes as ( + bytes: Uint8Array | Buffer, +) => boolean; +export const shouldIncludeFingerprintFile = contract.shouldIncludeFingerprintFile as (options: { + filePath: string; + isBinary?: boolean; + bytes?: Uint8Array | Buffer; + ignoreMatcher?: { ignores(path: string): boolean } | null; +}) => boolean; +export const sha256Hex = contract.sha256Hex as (bytes: Uint8Array | Buffer) => string; +export const buildSkillFingerprint = contract.buildSkillFingerprint as ( + files: Array<{ path: string; sha256: string }>, +) => string; +export const buildSkillFingerprintFromStoredFiles = + contract.buildSkillFingerprintFromStoredFiles as ( + files: Array<{ + file_path?: string; + path?: string; + content?: string; + is_binary?: number; + isBinary?: boolean; + encoding?: string; + }>, + options?: { ignoreMatcher?: { ignores(path: string): boolean } | null }, + ) => string; +export const fingerprintFromGoldenCase = contract.fingerprintFromGoldenCase as ( + testCase: { + ignoreFiles?: Array<{ path: string; content: string }>; + files: Array<{ + path: string; + content: string; + encoding?: string; + isBinary?: boolean; + }>; + }, +) => string; diff --git a/dt-skill/src/schema/textFiles.test.ts b/dt-skill/src/schema/textFiles.test.ts new file mode 100644 index 00000000..26309f99 --- /dev/null +++ b/dt-skill/src/schema/textFiles.test.ts @@ -0,0 +1,31 @@ +/* @vitest-environment node */ + +import { describe, expect, it } from "vitest"; +import * as schema from "."; +import { isTextContentType, TEXT_FILE_EXTENSION_SET } from "./textFiles"; + +describe("packages/clawhub schema textFiles", () => { + it("exports text-file extension set", () => { + expect(TEXT_FILE_EXTENSION_SET.has("md")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("r")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("ps1")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("psm1")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("psd1")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("tsv")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("conf")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("properties")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("dat")).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has("exe")).toBe(false); + }); + + it("detects text content types with parameters", () => { + expect(isTextContentType("text/plain; charset=utf-8")).toBe(true); + expect(isTextContentType("application/json; charset=utf-8")).toBe(true); + expect(isTextContentType("application/octet-stream")).toBe(false); + }); + + it("re-exports helpers from index", () => { + expect(typeof schema.isTextContentType).toBe("function"); + expect(schema.isTextContentType("application/markdown")).toBe(true); + }); +}); diff --git a/dt-skill/src/schema/textFiles.ts b/dt-skill/src/schema/textFiles.ts new file mode 100644 index 00000000..66f859ba --- /dev/null +++ b/dt-skill/src/schema/textFiles.ts @@ -0,0 +1,29 @@ +import { + TEXT_FILE_EXTENSIONS, + TEXT_FILE_EXTENSION_SET, +} from "./skillFingerprintContract.js"; + +const RAW_TEXT_CONTENT_TYPES = [ + "application/json", + "application/xml", + "application/yaml", + "application/x-yaml", + "application/toml", + "application/javascript", + "application/typescript", + "application/markdown", + "image/svg+xml", +] as const; + +export { TEXT_FILE_EXTENSIONS, TEXT_FILE_EXTENSION_SET }; + +export const TEXT_CONTENT_TYPES = RAW_TEXT_CONTENT_TYPES; +export const TEXT_CONTENT_TYPE_SET = new Set(TEXT_CONTENT_TYPES); + +export function isTextContentType(contentType: string) { + if (!contentType) return false; + const normalized = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? ""; + if (!normalized) return false; + if (normalized.startsWith("text/")) return true; + return TEXT_CONTENT_TYPE_SET.has(normalized); +} diff --git a/dt-skill/src/skills.test.ts b/dt-skill/src/skills.test.ts new file mode 100644 index 00000000..9e59ec85 --- /dev/null +++ b/dt-skill/src/skills.test.ts @@ -0,0 +1,284 @@ +/* @vitest-environment node */ +import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { strToU8, zipSync } from "fflate"; +import { describe, expect, it } from "vitest"; +import type { SkillOrigin } from "./skills"; +import { + buildSkillFingerprint, + extractZipToDir, + hashSkillFiles, + hashSkillZip, + listManualSkills, + listTextFiles, + readLockfile, + readSkillOrigin, + sha256Hex, + writeLockfile, + writeSkillOrigin, +} from "./skills"; + +describe("skills", () => { + it("extracts zip into directory and skips traversal", async () => { + const parent = await mkdtemp(join(tmpdir(), "clawhub-zip-")); + const dir = join(parent, "dir"); + await mkdir(dir); + const evilName = `evil-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; + const zip = zipSync({ + "SKILL.md": strToU8("hello"), + [`../${evilName}`]: strToU8("nope"), + }); + await extractZipToDir(new Uint8Array(zip), dir); + + expect((await readFile(join(dir, "SKILL.md"), "utf8")).trim()).toBe("hello"); + await expect(stat(join(parent, evilName))).rejects.toBeTruthy(); + }); + + it("writes and reads lockfile", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-work-")); + await writeLockfile(workdir, { + version: 1, + skills: { + demo: { + version: "1.0.0", + installedAt: 1, + pinned: true, + pinReason: "awaiting moderation review", + }, + }, + }); + const read = await readLockfile(workdir); + expect(read.skills.demo?.version).toBe("1.0.0"); + expect(read.skills.demo?.pinned).toBe(true); + expect(read.skills.demo?.pinReason).toBe("awaiting moderation review"); + }); + + it("returns empty lockfile on invalid json", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-work-bad-")); + await mkdir(join(workdir, ".clawhub"), { recursive: true }); + await writeFile(join(workdir, ".clawhub", "lock.json"), "{", "utf8"); + const read = await readLockfile(workdir); + expect(read).toEqual({ version: 1, skills: {} }); + }); + + it("returns empty lockfile on schema mismatch", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-work-schema-")); + await mkdir(join(workdir, ".clawhub"), { recursive: true }); + await writeFile( + join(workdir, ".clawhub", "lock.json"), + JSON.stringify({ version: 1, skills: "nope" }), + "utf8", + ); + const read = await readLockfile(workdir); + expect(read).toEqual({ version: 1, skills: {} }); + }); + + it("skips dotfiles and node_modules when listing text files", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-files-")); + await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); + await writeFile(join(workdir, ".secret.txt"), "no", "utf8"); + await mkdir(join(workdir, ".clawhub"), { recursive: true }); + await writeFile(join(workdir, ".clawhub", "origin.json"), "{}", "utf8"); + await mkdir(join(workdir, "node_modules"), { recursive: true }); + await writeFile(join(workdir, "node_modules", "a.txt"), "no", "utf8"); + const files = await listTextFiles(workdir); + expect(files.map((file) => file.relPath)).toEqual(["SKILL.md"]); + }); + + it("respects .gitignore and .clawhubignore", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-ignore-")); + await writeFile(join(workdir, ".gitignore"), "ignored.md\n", "utf8"); + await writeFile(join(workdir, ".clawhubignore"), "private.md\n", "utf8"); + await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); + await writeFile(join(workdir, "ignored.md"), "no", "utf8"); + await writeFile(join(workdir, "private.md"), "no", "utf8"); + await writeFile(join(workdir, "public.json"), "{}", "utf8"); + + const files = await listTextFiles(workdir); + const paths = files.map((file) => file.relPath).sort(); + expect(paths).toEqual(["SKILL.md", "public.json"]); + expect(files.find((file) => file.relPath === "SKILL.md")?.contentType).toMatch(/^text\//); + expect(files.find((file) => file.relPath === "public.json")?.contentType).toBe( + "application/json", + ); + }); + + it("falls back to text/plain for unknown text extensions", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-env-")); + await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); + await writeFile(join(workdir, "config.env"), "TOKEN=demo", "utf8"); + const files = await listTextFiles(workdir); + expect(files.find((file) => file.relPath === "config.env")?.contentType).toBe("text/plain"); + }); + + it("includes tsv and extensionless text files while skipping extensionless binaries", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-extensionless-")); + await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); + await writeFile(join(workdir, "config.tsv"), "name\tvalue\napi\tok\n", "utf8"); + await writeFile(join(workdir, ".npmrc"), "//registry.npmjs.org/:_authToken=secret\n", "utf8"); + await mkdir(join(workdir, "bin"), { recursive: true }); + await writeFile( + join(workdir, "bin", "openclaw-kraken"), + "#!/usr/bin/env sh\necho ok\n", + "utf8", + ); + const largeBinary = new Uint8Array(1024 * 1024); + largeBinary[0] = 0; + largeBinary[largeBinary.length - 1] = 255; + await writeFile(join(workdir, "bin", "binary"), largeBinary); + + const files = await listTextFiles(workdir); + const paths = files.map((file) => file.relPath).sort(); + expect(paths).toEqual(["SKILL.md", "bin/openclaw-kraken", "config.tsv"]); + expect(files.find((file) => file.relPath === "bin/openclaw-kraken")?.contentType).toBe( + "text/plain", + ); + }); + + it("hashes skill files deterministically", async () => { + const { fingerprint } = hashSkillFiles([ + { relPath: "b.txt", bytes: strToU8("b") }, + { relPath: "a.txt", bytes: strToU8("a") }, + ]); + const expected = buildSkillFingerprint([ + { path: "a.txt", sha256: sha256Hex(strToU8("a")) }, + { path: "b.txt", sha256: sha256Hex(strToU8("b")) }, + ]); + expect(fingerprint).toBe(expected); + }); + + it("hashes text files inside a downloaded zip deterministically", () => { + const zip = zipSync({ + "SKILL.md": strToU8("hello"), + "notes.md": strToU8("world"), + ".npmrc": strToU8("//registry.npmjs.org/:_authToken=secret\n"), + "config/endpoints.tsv": strToU8("name\turl\napi\thttps://example.com\n"), + "bin/tool": strToU8("#!/usr/bin/env sh\necho ok\n"), + "image.png": strToU8("nope"), + }); + const { fingerprint } = hashSkillZip(new Uint8Array(zip)); + const expected = buildSkillFingerprint([ + { path: "SKILL.md", sha256: sha256Hex(strToU8("hello")) }, + { path: "bin/tool", sha256: sha256Hex(strToU8("#!/usr/bin/env sh\necho ok\n")) }, + { + path: "config/endpoints.tsv", + sha256: sha256Hex(strToU8("name\turl\napi\thttps://example.com\n")), + }, + { path: "notes.md", sha256: sha256Hex(strToU8("world")) }, + ]); + expect(fingerprint).toBe(expected); + }); + + it("ignores unsafe or non-text entries when hashing zips", () => { + const zip = zipSync({ + "SKILL.md": strToU8("hello"), + "folder/": strToU8(""), + "../evil.txt": strToU8("nope"), + "bad\\path.txt": strToU8("nope"), + "image.png": strToU8("nope"), + }); + const { files } = hashSkillZip(new Uint8Array(zip)); + expect(files).toEqual([{ path: "SKILL.md", sha256: sha256Hex(strToU8("hello")), size: 5 }]); + }); + + it("builds fingerprints from valid entries only", () => { + const fingerprint = buildSkillFingerprint([ + { path: "", sha256: "" }, + { path: "valid.txt", sha256: sha256Hex(strToU8("ok")) }, + ]); + const expected = buildSkillFingerprint([ + { path: "valid.txt", sha256: sha256Hex(strToU8("ok")) }, + ]); + expect(fingerprint).toBe(expected); + }); + + it("returns null for invalid skill origin metadata", async () => { + const workdir = await mkdtemp(join(tmpdir(), "clawhub-origin-")); + expect(await readSkillOrigin(workdir)).toBeNull(); + + await mkdir(join(workdir, ".clawhub"), { recursive: true }); + await writeFile( + join(workdir, ".clawhub", "origin.json"), + JSON.stringify({ version: 2 }), + "utf8", + ); + expect(await readSkillOrigin(workdir)).toBeNull(); + + await writeFile( + join(workdir, ".clawhub", "origin.json"), + JSON.stringify({ version: 1, registry: "demo", slug: "x", installedAt: 1 }), + "utf8", + ); + expect(await readSkillOrigin(workdir)).toBeNull(); + + await writeFile( + join(workdir, ".clawhub", "origin.json"), + JSON.stringify({ + version: 1, + registry: "demo", + slug: "x", + installedVersion: "0.1.0", + installedAt: "nope", + }), + "utf8", + ); + expect(await readSkillOrigin(workdir)).toBeNull(); + + const origin: SkillOrigin = { + version: 1, + registry: "https://example.com", + slug: "demo", + installedVersion: "1.2.3", + installedAt: 123, + }; + await writeSkillOrigin(workdir, origin); + expect(await readSkillOrigin(workdir)).toEqual(origin); + }); + + describe("listManualSkills", () => { + it("lists manual skills not present in the lockfile", async () => { + const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-")); + await mkdir(join(dir, "manual-skill")); + await writeFile(join(dir, "manual-skill", "SKILL.md"), "# Manual", "utf8"); + + await mkdir(join(dir, "tracked-skill")); + await writeFile(join(dir, "tracked-skill", "SKILL.md"), "# Tracked", "utf8"); + + const result = await listManualSkills(dir, new Set(["tracked-skill"])); + expect(result).toEqual(["manual-skill"]); + }); + + it("recognizes skills from current and legacy origin metadata", async () => { + const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-origin-")); + await mkdir(join(dir, "current", ".clawhub"), { recursive: true }); + await writeFile(join(dir, "current", ".clawhub", "origin.json"), "{}", "utf8"); + await mkdir(join(dir, "legacy", ".clawdhub"), { recursive: true }); + await writeFile(join(dir, "legacy", ".clawdhub", "origin.json"), "{}", "utf8"); + + const result = await listManualSkills(dir, new Set()); + expect(result).toEqual(["current", "legacy"]); + }); + + it("skips hidden and non-skill directories and returns sorted results", async () => { + const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-sort-")); + await mkdir(join(dir, "z-skill")); + await writeFile(join(dir, "z-skill", "SKILL.md"), "# Z", "utf8"); + await mkdir(join(dir, "a-skill")); + await writeFile(join(dir, "a-skill", "SKILL.md"), "# A", "utf8"); + await mkdir(join(dir, ".hidden")); + await writeFile(join(dir, ".hidden", "SKILL.md"), "# Hidden", "utf8"); + await mkdir(join(dir, "notes")); + await writeFile(join(dir, "notes", "README.md"), "not a skill", "utf8"); + + const result = await listManualSkills(dir, new Set()); + expect(result).toEqual(["a-skill", "z-skill"]); + }); + + it("returns an empty list when the skills directory does not exist", async () => { + const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-missing-")); + const result = await listManualSkills(join(dir, "missing"), new Set()); + expect(result).toEqual([]); + }); + }); +}); diff --git a/dt-skill/src/skills.ts b/dt-skill/src/skills.ts new file mode 100644 index 00000000..ed17607c --- /dev/null +++ b/dt-skill/src/skills.ts @@ -0,0 +1,255 @@ +import { access, mkdir, open, readdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { unzipSync } from "fflate"; +import ignore from "ignore"; +import mime from "mime"; +import { + type Lockfile, + LockfileSchema, + parseArk, +} from "./schema/index.js"; +import { + buildSkillFingerprint, + getFileExtension, + hasDotPathSegment, + isLikelyTextBytes, + sha256Hex, + shouldIncludeFingerprintFile, + TEXT_FILE_EXTENSION_SET, + TEXT_SAMPLE_BYTES, +} from "./schema/skillFingerprintContract.js"; + +const DOT_DIR = ".clawhub"; +const LEGACY_DOT_DIR = ".clawdhub"; +const DOT_IGNORE = ".clawhubignore"; +const LEGACY_DOT_IGNORE = ".clawdhubignore"; + +export type SkillOrigin = { + version: 1; + registry: string; + slug: string; + installedVersion: string; + installedAt: number; + fingerprint?: string; +}; + +export async function extractZipToDir(zipBytes: Uint8Array, targetDir: string) { + const entries = unzipSync(zipBytes); + await mkdir(targetDir, { recursive: true }); + for (const [rawPath, data] of Object.entries(entries)) { + const safePath = sanitizeRelPath(rawPath); + if (!safePath) continue; + const outPath = join(targetDir, safePath); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, data); + } +} + +export async function listTextFiles(root: string) { + const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; + const absRoot = resolve(root); + const ig = ignore(); + ig.add([".git/", "node_modules/", `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`]); + await addIgnoreFile(ig, join(absRoot, ".gitignore")); + await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); + await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE)); + + await walk(absRoot, async (absPath) => { + const relPath = normalizePath(relative(absRoot, absPath)); + if (!relPath) return; + if (ig.ignores(relPath)) return; + if (hasDotPathSegment(relPath)) return; + const ext = getFileExtension(relPath); + if (ext && !TEXT_FILE_EXTENSION_SET.has(ext)) return; + if (!ext && !(await isLikelyTextFile(absPath))) return; + const buffer = await readFile(absPath); + const contentType = mime.getType(relPath) ?? "text/plain"; + files.push({ relPath, bytes: new Uint8Array(buffer), contentType }); + }); + return files; +} + +type SkillFileHash = { path: string; sha256: string; size: number }; + +export { buildSkillFingerprint, sha256Hex }; + +export function hashSkillFiles(files: Array<{ relPath: string; bytes: Uint8Array }>) { + const hashed = files.map((file) => ({ + path: file.relPath, + sha256: sha256Hex(file.bytes), + size: file.bytes.byteLength, + })); + return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; +} + +export function hashSkillZip(zipBytes: Uint8Array) { + const entries = unzipSync(zipBytes); + const hashed = Object.entries(entries) + .map(([rawPath, bytes]) => { + const safePath = sanitizeZipPath(rawPath); + if (!safePath) return null; + if ( + !shouldIncludeFingerprintFile({ + filePath: safePath, + bytes, + }) + ) { + return null; + } + return { path: safePath, sha256: sha256Hex(bytes), size: bytes.byteLength }; + }) + .filter(Boolean) as SkillFileHash[]; + + return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; +} + +export async function readLockfile(workdir: string): Promise { + const paths = [join(workdir, DOT_DIR, "lock.json"), join(workdir, LEGACY_DOT_DIR, "lock.json")]; + for (const path of paths) { + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as unknown; + return parseArk(LockfileSchema, parsed, "Lockfile"); + } catch { + // try next + } + } + return { version: 1, skills: {} }; +} + +export async function writeLockfile(workdir: string, lock: Lockfile) { + const path = join(workdir, DOT_DIR, "lock.json"); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, "utf8"); +} + +export async function readSkillOrigin(skillFolder: string): Promise { + const paths = [ + join(skillFolder, DOT_DIR, "origin.json"), + join(skillFolder, LEGACY_DOT_DIR, "origin.json"), + ]; + for (const path of paths) { + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as Partial; + if (parsed.version !== 1) return null; + if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null; + if (typeof parsed.installedAt !== "number" || !Number.isFinite(parsed.installedAt)) { + return null; + } + return { + version: 1, + registry: parsed.registry, + slug: parsed.slug, + installedVersion: parsed.installedVersion, + installedAt: parsed.installedAt, + fingerprint: typeof parsed.fingerprint === "string" ? parsed.fingerprint : undefined, + }; + } catch { + // try next + } + } + return null; +} + +export async function writeSkillOrigin(skillFolder: string, origin: SkillOrigin) { + const path = join(skillFolder, DOT_DIR, "origin.json"); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(origin, null, 2)}\n`, "utf8"); +} + +function normalizePath(path: string) { + return path + .split(sep) + .join("/") + .replace(/^\.\/+/, ""); +} + +async function isLikelyTextFile(path: string) { + const handle = await open(path, "r"); + try { + const sample = new Uint8Array(TEXT_SAMPLE_BYTES); + const { bytesRead } = await handle.read(sample, 0, sample.byteLength, 0); + return isLikelyTextBytes(sample.subarray(0, bytesRead)); + } finally { + await handle.close(); + } +} + +function sanitizeRelPath(path: string) { + const normalized = path.replace(/^\.\/+/, "").replace(/^\/+/, ""); + if (!normalized || normalized.endsWith("/")) return null; + if (normalized.includes("..") || normalized.includes("\\")) return null; + return normalized; +} + +function sanitizeZipPath(path: string) { + return sanitizeRelPath(path); +} + +async function walk(dir: string, onFile: (path: string) => Promise) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + if (entry.name === "node_modules") continue; + const full = join(dir, entry.name); + if (entry.isDirectory()) { + await walk(full, onFile); + continue; + } + if (!entry.isFile()) continue; + await onFile(full); + } +} + +async function addIgnoreFile(ig: ReturnType, path: string) { + try { + const raw = await readFile(path, "utf8"); + ig.add(raw.split(/\r?\n/)); + } catch { + // optional + } +} + +export async function listManualSkills(skillsDir: string, lockedSlugs: Set) { + const manual: string[] = []; + let entries; + try { + entries = await readdir(skillsDir, { withFileTypes: true }); + } catch (error) { + if (isMissingPathError(error)) return manual; + throw error; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (entry.name.startsWith(".")) continue; + if (lockedSlugs.has(entry.name)) continue; + if (await hasSkillMetadata(join(skillsDir, entry.name))) { + manual.push(entry.name); + } + } + return manual.sort((a, b) => a.localeCompare(b)); +} + +async function hasSkillMetadata(skillDir: string) { + const candidates = [ + join(skillDir, "SKILL.md"), + join(skillDir, DOT_DIR, "origin.json"), + join(skillDir, LEGACY_DOT_DIR, "origin.json"), + ]; + for (const path of candidates) { + try { + await access(path); + return true; + } catch (error) { + if (!isMissingPathError(error)) throw error; + } + } + return false; +} + +function isMissingPathError(error: unknown) { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + return code === "ENOENT" || code === "ENOTDIR"; +} diff --git a/dt-skill/test-artifact/cli.artifact.test.ts b/dt-skill/test-artifact/cli.artifact.test.ts new file mode 100644 index 00000000..b16df1ed --- /dev/null +++ b/dt-skill/test-artifact/cli.artifact.test.ts @@ -0,0 +1,157 @@ +/* @vitest-environment node */ + +import { spawnSync } from 'node:child_process'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; + +const packageRoot = resolve(import.meta.dirname, '..'); +const repoRoot = resolve(packageRoot, '..', '..'); +const binPath = join(packageRoot, 'bin', 'dt-skill.js'); +const distCliPath = join(packageRoot, 'dist', 'cli.js'); + +const tempDirs: string[] = []; + +async function makeTmpDir(prefix: string) { + const dir = await mkdtemp(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +function runNode(args: string[], envOverrides: NodeJS.ProcessEnv = {}) { + const { FORCE_COLOR: _forceColor, ...env } = process.env; + return spawnSync('node', args, { + cwd: repoRoot, + encoding: 'utf8', + env: { ...env, ...envOverrides }, + }); +} + +function runGit(cwd: string, args: string[]) { + const result = spawnSync('git', ['-C', cwd, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + if (result.status !== 0) { + throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`); + } + return result.stdout.trim(); +} + +afterEach(async () => { + while (tempDirs.length > 0) { + await rm(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +describe('built CLI artifact', () => { + it('runs help from the published bin entrypoint', async () => { + const result = runNode([binPath, '--help']); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + expect(result.stdout).toContain('dt-skill CLI'); + }); + + it('publishes a local code plugin in dry-run json mode from built output', async () => { + const root = await makeTmpDir('clawhub-artifact-'); + const pluginDir = join(root, 'demo-plugin'); + await mkdir(join(pluginDir, 'src'), { recursive: true }); + await writeFile( + join(pluginDir, 'package.json'), + JSON.stringify({ + name: '@openclaw/demo-plugin', + displayName: 'Demo Plugin', + version: '1.0.0', + openclaw: { + compat: { + pluginApi: '>=2026.3.24-beta.2', + minGatewayVersion: '2026.3.24-beta.2', + }, + build: { + openclawVersion: '2026.3.24-beta.2', + pluginSdkVersion: '2026.3.24-beta.2', + }, + }, + }), + 'utf8' + ); + await writeFile( + join(pluginDir, 'openclaw.plugin.json'), + JSON.stringify({ + id: 'demo.plugin', + configSchema: { + type: 'object', + additionalProperties: false, + }, + }), + 'utf8' + ); + await writeFile(join(pluginDir, 'src', 'index.ts'), 'export const demo = true;\n', 'utf8'); + + runGit(root, ['init']); + runGit(root, ['remote', 'add', 'origin', 'https://github.com/openclaw/demo-plugin.git']); + runGit(root, ['add', '.']); + runGit(root, [ + '-c', + 'user.name=Test', + '-c', + 'user.email=test@example.com', + 'commit', + '-m', + 'init', + ]); + + const result = runNode( + [ + binPath, + 'package', + 'publish', + pluginDir, + '--dry-run', + '--json', + '--registry', + 'https://clawhub.ai', + '--site', + 'https://clawhub.ai', + ], + { NPM_CONFIG_CACHE: join(root, '.npm-cache') } + ); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + const output = JSON.parse(result.stdout.trim()) as Record; + expect(output.name).toBe('@openclaw/demo-plugin'); + expect(output.family).toBe('code-plugin'); + expect(output.version).toBe('1.0.0'); + expect(output.commit).toBeTypeOf('string'); + }); + + it('keeps the built dist free of compiled test files', async () => { + expect(dirname(distCliPath)).toBe(join(packageRoot, 'dist')); + const result = runNode([ + '--input-type=module', + '--eval', + `import { readdir } from 'node:fs/promises'; + import { join } from 'node:path'; + const queue = ['${join(packageRoot, 'dist').replaceAll('\\', '\\\\')}']; + const hits = []; + while (queue.length > 0) { + const dir = queue.pop(); + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) queue.push(path); + else if (entry.name.includes('.test.')) hits.push(path); + } + } + if (hits.length > 0) { + console.error(hits.join('\\n')); + process.exit(1); + }`, + ]); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + }); +}); diff --git a/dt-skill/test/cliCommandTestKit.ts b/dt-skill/test/cliCommandTestKit.ts new file mode 100644 index 00000000..5983dbb7 --- /dev/null +++ b/dt-skill/test/cliCommandTestKit.ts @@ -0,0 +1,101 @@ +import { join } from "node:path"; +import { vi } from "vitest"; +import type { GlobalOpts } from "../src/cli/types.js"; + +export function makeGlobalOpts(workdir = "/work"): GlobalOpts { + return { + workdir, + dir: join(workdir, "skills"), + site: "https://clawhub.ai", + registry: "https://clawhub.ai", + registrySource: "default", + }; +} + +function buildRegistryUrl(path: string, registry: string) { + const base = registry.endsWith("/") ? registry : `${registry}/`; + const relative = path.startsWith("/") ? path.slice(1) : path; + return new URL(relative, base); +} + +export function createHttpModuleMocks() { + const apiRequest = vi.fn(); + const apiRequestForm = vi.fn(); + const downloadZip = vi.fn(); + const fetchBinary = vi.fn(); + const fetchText = vi.fn(); + const registryUrl = vi.fn(buildRegistryUrl); + + return { + apiRequest, + apiRequestForm, + downloadZip, + fetchBinary, + fetchText, + registryUrl, + moduleFactory: () => ({ + apiRequest: (registry: unknown, args: unknown, schema?: unknown) => + apiRequest(registry, args, schema), + apiRequestForm: (registry: unknown, args: unknown, schema?: unknown) => + apiRequestForm(registry, args, schema), + downloadZip: (registry: unknown, args: unknown) => downloadZip(registry, args), + fetchBinary: (registry: unknown, args: unknown) => fetchBinary(registry, args), + fetchText: (registry: unknown, args: unknown) => fetchText(registry, args), + registryUrl: (...args: [string, string]) => registryUrl(...args), + }), + }; +} + +export function createRegistryModuleMocks() { + const getRegistry = vi.fn(async (_opts?: unknown, _params?: unknown) => "https://clawhub.ai"); + + return { + getRegistry, + moduleFactory: () => ({ + getRegistry: (opts: unknown, params?: unknown) => getRegistry(opts, params), + }), + }; +} + +export function createAuthTokenModuleMocks() { + const requireAuthToken = vi.fn(async () => "tkn"); + const getOptionalAuthToken = vi.fn(async () => undefined as string | undefined); + + return { + requireAuthToken, + getOptionalAuthToken, + moduleFactory: () => ({ + requireAuthToken: () => requireAuthToken(), + getOptionalAuthToken: () => getOptionalAuthToken(), + }), + }; +} + +export function createUiModuleMocks(options?: { interactive?: boolean }) { + const spinner = { + stop: vi.fn(), + fail: vi.fn(), + succeed: vi.fn(), + start: vi.fn(), + isSpinning: false, + text: "", + }; + const fail = vi.fn((message: string) => { + throw new Error(message); + }); + const promptConfirm = vi.fn(async () => true); + const interactive = options?.interactive ?? false; + + return { + spinner, + fail, + promptConfirm, + moduleFactory: () => ({ + createSpinner: vi.fn(() => spinner), + fail: (message: string) => fail(message), + formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), + isInteractive: () => interactive, + promptConfirm, + }), + }; +} diff --git a/dt-skill/test/runtimeStubs.ts b/dt-skill/test/runtimeStubs.ts new file mode 100644 index 00000000..dc359a7a --- /dev/null +++ b/dt-skill/test/runtimeStubs.ts @@ -0,0 +1,54 @@ +export function createGlobalStubRegistry() { + const restorers: Array<() => void> = []; + + return { + stub(name: K, value: (typeof globalThis)[K]) { + const original = globalThis[name]; + restorers.push(() => { + if (original === undefined) { + Reflect.deleteProperty(globalThis, name); + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + writable: true, + value: original, + }); + }); + Object.defineProperty(globalThis, name, { + configurable: true, + writable: true, + value, + }); + }, + restoreAll() { + while (restorers.length > 0) { + restorers.pop()?.(); + } + }, + }; +} + +export function createEnvStubRegistry() { + const restorers: Array<() => void> = []; + + return { + stub(name: string, value: string) { + const original = process.env[name]; + const hadOriginal = Object.prototype.hasOwnProperty.call(process.env, name); + restorers.push(() => { + if (hadOriginal) { + process.env[name] = original; + return; + } + delete process.env[name]; + }); + process.env[name] = value; + }, + restoreAll() { + while (restorers.length > 0) { + restorers.pop()?.(); + } + }, + }; +} diff --git a/dt-skill/tsconfig.json b/dt-skill/tsconfig.json new file mode 100644 index 00000000..25581cbe --- /dev/null +++ b/dt-skill/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "declaration": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/dt-skill/vitest.artifact.config.ts b/dt-skill/vitest.artifact.config.ts new file mode 100644 index 00000000..bf44a678 --- /dev/null +++ b/dt-skill/vitest.artifact.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: false, + testTimeout: 30_000, + hookTimeout: 30_000, + include: ["test-artifact/**/*.test.ts"], + exclude: ["dist/**", "node_modules/**"], + }, +}); diff --git a/dt-skill/vitest.config.ts b/dt-skill/vitest.config.ts new file mode 100644 index 00000000..250df383 --- /dev/null +++ b/dt-skill/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: false, + testTimeout: 15_000, + hookTimeout: 15_000, + include: ["src/**/*.test.ts"], + exclude: ["dist/**", "node_modules/**", "test-artifact/**", "src/cli/commands/packages.test.ts"], + }, +}); diff --git a/package.json b/package.json index c100cfcf..a1cafa9f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "clean": "easy clean", "build": "NODE_OPTIONS=--openssl-legacy-provider && easy build", "debug": "egg-bin debug", - "dev": "NODE_OPTIONS=--openssl-legacy-provider && egg-bin dev --daemon", + "dev": "NODE_OPTIONS=--openssl-legacy-provider egg-bin dev --daemon", "start": "bash start.sh", "start:test": "bash start.sh -t", "server": "egg-scripts start --daemon --workers=4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..b93634a1 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,24801 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ant-design/icons': + specifier: 4.5.0 + version: 4.5.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + '@modelcontextprotocol/sdk': + specifier: ^1.25.2 + version: 1.29.0(zod@4.4.3) + adm-zip: + specifier: ^0.5.10 + version: 0.5.17 + ant-design-dtinsight-theme: + specifier: 1.1.3 + version: 1.1.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + antd: + specifier: 4.15.6 + version: 4.15.6(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + await-stream-ready: + specifier: ^1.0.1 + version: 1.0.1 + babel-eslint: + specifier: ^10.1.0 + version: 10.1.0(eslint@8.22.0) + babel-plugin-transform-decorators-legacy: + specifier: ^1.3.5 + version: 1.3.5 + cheerio: + specifier: ^1.0.0-rc.10 + version: 1.2.0 + codemirror: + specifier: ^5.48.4 + version: 5.65.21 + content-type: + specifier: ^1.0.5 + version: 1.0.5 + cropperjs: + specifier: ^1.5.1 + version: 1.6.2 + dingtalk-robot-sender: + specifier: ^1.2.0 + version: 1.2.0 + egg: + specifier: ^2.1.0 + version: 2.37.0 + egg-cors: + specifier: ^2.0.0 + version: 2.2.4 + egg-logger: + specifier: ^1.5.0 + version: 1.8.0 + egg-scripts: + specifier: ^2.8.1 + version: 2.17.0 + egg-sequelize: + specifier: ^4.3.1 + version: 4.3.1 + egg-socket.io: + specifier: ^4.1.6 + version: 4.1.6 + egg-ssh: + specifier: ^1.0.5 + version: 1.0.5 + egg-validate: + specifier: ^1.0.0 + version: 1.1.2 + egg-view-react-ssr: + specifier: ^2.2.6 + version: 2.5.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + extend: + specifier: ~3.0.0 + version: 3.0.2 + file-saver: + specifier: ^1.3.3 + version: 1.3.8 + history: + specifier: ^4.7.2 + version: 4.10.1 + html2canvas: + specifier: ^0.5.0-beta4 + version: 0.5.0-beta4 + http-proxy-middleware: + specifier: 2.0.6 + version: 2.0.6 + js-cookie: + specifier: ^2.2.0 + version: 2.2.1 + koa-connect: + specifier: ^2.0.1 + version: 2.1.1 + lodash: + specifier: ^4.17.4 + version: 4.18.1 + mockjs: + specifier: ^1.0.1-beta3 + version: 1.1.0 + moment: + specifier: ^2.17.1 + version: 2.30.1 + mysql2: + specifier: ^1.6.5 + version: 1.7.0 + node-fetch: + specifier: '2' + version: 2.7.0(encoding@0.1.13) + node-schedule: + specifier: ^2.0.0 + version: 2.1.1 + node-ssh: + specifier: ^6.0.0 + version: 6.0.0 + raw-body: + specifier: ^3.0.1 + version: 3.0.2 + react: + specifier: 16.9.0 + version: 16.9.0 + react-codemirror2: + specifier: ^7.2.1 + version: 7.3.0(codemirror@5.65.21)(react@16.9.0) + react-color: + specifier: ^2.19.3 + version: 2.19.3(react@16.9.0) + react-dom: + specifier: ^16.9.0 + version: 16.14.0(react@16.9.0) + react-loadable: + specifier: ^5.5.0 + version: 5.5.0(react@16.9.0) + react-markdown: + specifier: ^6.0.3 + version: 6.0.3(@types/react@16.14.70)(react@16.9.0) + react-redux: + specifier: ^7.1.0 + version: 7.2.9(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react-router: + specifier: ^4.2.0 + version: 4.3.1(react@16.9.0) + react-router-config: + specifier: ^1.0.0-beta.4 + version: 1.0.0-beta.4(react-router@4.3.1(react@16.9.0))(react@16.9.0) + react-router-dom: + specifier: ^4.2.2 + version: 4.3.1(react@16.9.0) + react-router-redux: + specifier: ^4.0.8 + version: 4.0.8 + react-syntax-highlighter: + specifier: ^15.6.0 + version: 15.6.6(react@16.9.0) + redux: + specifier: ^4.0.4 + version: 4.2.1 + redux-thunk: + specifier: ^2.3.0 + version: 2.4.2(redux@4.2.1) + remark-gfm: + specifier: ^1.0.0 + version: 1.0.0 + socket.io: + specifier: ^4.1.0 + version: 4.8.3 + socket.io-client: + specifier: 1.7.0 + version: 1.7.0 + ssh2: + specifier: ^1.4.0 + version: 1.17.0 + stream-buffers: + specifier: ^3.0.2 + version: 3.0.3 + tar: + specifier: ^6.1.15 + version: 6.2.1 + typescript: + specifier: 4.7.4 + version: 4.7.4 + utf8: + specifier: ^3.0.0 + version: 3.0.0 + uuid: + specifier: ^8.1.0 + version: 8.3.2 + xterm: + specifier: ^4.12.0 + version: 4.19.0 + xterm-addon-attach: + specifier: ^0.6.0 + version: 0.6.0(xterm@4.19.0) + xterm-addon-fit: + specifier: ^0.5.0 + version: 0.5.0(xterm@4.19.0) + devDependencies: + '@babel/plugin-syntax-dynamic-import': + specifier: ^7.8.3 + version: 7.8.3(@babel/core@7.29.0) + '@commitlint/config-conventional': + specifier: ^8.2.0 + version: 8.3.6 + '@hot-loader/react-dom': + specifier: ^17.0.1 + version: 17.0.2(react@16.9.0) + '@types/classnames': + specifier: ^2.2.11 + version: 2.3.4 + '@types/file-saver': + specifier: ^2.0.1 + version: 2.0.7 + '@types/html2canvas': + specifier: ^0.0.36 + version: 0.0.36 + '@types/js-cookie': + specifier: ^2.2.6 + version: 2.2.7 + '@types/react': + specifier: ^16.0.0 + version: 16.14.70 + '@types/react-color': + specifier: ^3.0.4 + version: 3.0.13(@types/react@16.14.70) + '@types/react-dom': + specifier: ^16.0.0 + version: 16.9.25(@types/react@16.14.70) + '@types/react-redux': + specifier: ^7.1.16 + version: 7.1.34 + '@types/react-router-config': + specifier: ^5.0.2 + version: 5.0.11 + '@types/react-router-dom': + specifier: ^5.1.7 + version: 5.3.3 + babel-preset-stage-0: + specifier: ^6.24.1 + version: 6.24.1 + babel-preset-stage-2: + specifier: ^6.24.1 + version: 6.24.1 + commitizen: + specifier: ^4.0.3 + version: 4.3.1(@types/node@25.9.1)(typescript@4.7.4) + commitlint: + specifier: ^8.2.0 + version: 8.3.6 + conventional-changelog-cli: + specifier: ^2.0.28 + version: 2.2.2 + cz-conventional-changelog: + specifier: ^3.3.0 + version: 3.3.0(@types/node@25.9.1)(typescript@4.7.4) + docsify-cli: + specifier: ^4.4.3 + version: 4.4.4(encoding@0.1.13) + easywebpack-cli: + specifier: ^4.3.5 + version: 4.8.1(webpack@4.47.0) + easywebpack-react: + specifier: ^4.4.1 + version: 4.4.5(@types/react@16.14.70)(eslint@8.22.0)(react-dom@16.14.0(react@16.9.0))(react@16.9.0)(typescript@4.7.4)(webpack@4.47.0) + egg-bin: + specifier: ^4.5.0 + version: 4.20.0(@types/node@25.9.1) + egg-webpack: + specifier: ^4.4.9 + version: 4.5.5 + egg-webpack-react: + specifier: ^2.0.2 + version: 2.0.3 + eslint-config-egg: + specifier: ^5.1.1 + version: 5.1.1(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0) + eslint-plugin-react: + specifier: ^7.1.0 + version: 7.37.5(eslint@8.22.0) + file-loader: + specifier: ^3.0.1 + version: 3.0.1(webpack@4.47.0) + husky: + specifier: ^3.1.0 + version: 3.1.0 + ip: + specifier: ^1.1.5 + version: 1.1.9 + ko-lint-config: + specifier: ^2.2.21 + version: 2.2.22(typescript@4.7.4) + less: + specifier: 3.9.0 + version: 3.9.0 + less-loader: + specifier: ^4.0.0 + version: 4.1.0(less@3.9.0)(webpack@4.47.0) + redux-devtools-extension: + specifier: ^2.13.8 + version: 2.13.9(redux@4.2.1) + request: + specifier: ^2.88.2 + version: 2.88.2 + sass: + specifier: ^1.77.2 + version: 1.99.0 + sass-loader: + specifier: 10.2.0 + version: 10.2.0(sass@1.99.0)(webpack@4.47.0) + sequelize-cli: + specifier: ^5.4.0 + version: 5.5.1 + standard-version: + specifier: ^9.0.0 + version: 9.5.0 + ts-loader: + specifier: ^8.0.17 + version: 8.4.0(typescript@4.7.4)(webpack@4.47.0) + url-loader: + specifier: ^1.1.2 + version: 1.1.2(webpack@4.47.0) + vue-cli-plugin-commitlint: + specifier: ^1.0.4 + version: 1.0.12 + webpack: + specifier: ^4.30.0 + version: 4.47.0 + +packages: + + '@ant-design/colors@3.2.2': + resolution: {integrity: sha512-YKgNbG2dlzqMhA9NtI3/pbY16m3Yl/EeWBRa+lB1X1YaYxHrxNexiQYCLTWO/uDvAjLFMEDU+zR901waBtMtjQ==} + + '@ant-design/colors@6.0.0': + resolution: {integrity: sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==} + + '@ant-design/create-react-context@0.2.6': + resolution: {integrity: sha512-pHUuaE50/WEek4w2Q+QYVieLPIGfXM+nUsGSsg8xO6oHBw7dfd14Ws/6q3/L6eZ60zjUiv3WUlSzpWyCOXLqbQ==} + peerDependencies: + prop-types: '>=15.0.0' + react: ^0.14.0 || >=15.0.0 + + '@ant-design/css-animation@1.7.3': + resolution: {integrity: sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA==} + + '@ant-design/icons-react@2.0.1': + resolution: {integrity: sha512-r1QfoltMuruJZqdiKcbPim3d8LNsVPB733U0gZEUSxBLuqilwsW28K2rCTWSMTjmFX7Mfpf+v/wdiFe/XCqThw==} + peerDependencies: + '@ant-design/icons': ^2.0.0 + react: 16.x + + '@ant-design/icons-svg@4.4.2': + resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} + + '@ant-design/icons@2.1.1': + resolution: {integrity: sha512-jCH+k2Vjlno4YWl6g535nHR09PwCEmTBKAG6VqF+rhkrSPRLfgpU2maagwbZPLjaHuU5Jd1DFQ2KJpQuI6uG8w==} + + '@ant-design/icons@4.5.0': + resolution: {integrity: sha512-ZAKJcmr4DBV3NWr8wm2dCxNKN4eFrX+qCaPsuFejP6FRsf+m5OKxvCVi9bSp1lmKWeOI5yECAx5s0uFm4QHuPw==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + + '@ant-design/icons@4.8.3': + resolution: {integrity: sha512-HGlIQZzrEbAhpJR6+IGdzfbPym94Owr6JZkJ2QCCnOkPVIWMO2xgIVcOKnl8YcpijIo39V7l2qQL5fmtw56cMw==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/react-slick@0.28.4': + resolution: {integrity: sha512-j9eAHTn7GxbXUFNknJoHS2ceAsqrQi2j8XykjZE1IXCD8kJF+t28EvhBLniDpbOsBk/3kjalnhriTfZcjBHNqg==} + peerDependencies: + react: '>=16.9.0' + + '@babel/code-frame@7.0.0-beta.44': + resolution: {integrity: sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.0.0-beta.44': + resolution: {integrity: sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ==} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-function-name@7.0.0-beta.44': + resolution: {integrity: sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg==} + + '@babel/helper-get-function-arity@7.0.0-beta.44': + resolution: {integrity: sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw==} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.0.0-beta.44': + resolution: {integrity: sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA==} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.0.0-beta.44': + resolution: {integrity: sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ==} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/polyfill@7.12.1': + resolution: {integrity: sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==} + deprecated: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information. + + '@babel/runtime-corejs3@7.29.2': + resolution: {integrity: sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.0.0-beta.44': + resolution: {integrity: sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng==} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.0.0-beta.44': + resolution: {integrity: sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA==} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.0.0-beta.44': + resolution: {integrity: sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ==} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@commitlint/cli@8.3.6': + resolution: {integrity: sha512-fg8p9/ZrzhUPIXBGrpzwKu50WT13jYS5OffYlkStPuemuv0GjXu37B8J/zNgu6UhrdBVHbmBR0LriKAzRLG/4g==} + engines: {node: '>=4'} + hasBin: true + + '@commitlint/config-conventional@8.3.6': + resolution: {integrity: sha512-sbjDwFoa0on+IUbnBswd1ZTa8bkcDlzgWS/s2HapwNw8CBHBuoJbBDOQBqaYLI4b4O7SHYyArMx3V1FvUXTVsg==} + + '@commitlint/config-validator@21.0.1': + resolution: {integrity: sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA==} + engines: {node: '>=22.12.0'} + + '@commitlint/ensure@8.3.6': + resolution: {integrity: sha512-UUipnA7sX3OSUW39pi4Etf7pKrG76uM33ybs5YTEOZbT6zb3aKUS+A1ygo52eX+tqpxCiV+6qSy5qEKG8c1aeA==} + engines: {node: '>=4'} + + '@commitlint/execute-rule@21.0.1': + resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==} + engines: {node: '>=22.12.0'} + + '@commitlint/execute-rule@8.3.6': + resolution: {integrity: sha512-kCcf+33LgFBZcVKzTRX7QZBiznFjzjgpyEXFjGsWgCeOXi1q3KPdwH9HvH22xpFZ4+n4lAuv/kQf5XUQMO2OGQ==} + engines: {node: '>=4'} + + '@commitlint/format@8.3.6': + resolution: {integrity: sha512-VN9Yq3cJoonLjeoYiTOidsxGM6lwyzcw6ekQCCIzjNbJa+7teTPE2wDSXqhbsF/0XDJUeHcygzgZwv4/lzStTA==} + engines: {node: '>=4'} + + '@commitlint/is-ignored@8.3.6': + resolution: {integrity: sha512-wxQImxePfAfIz9C2nWzebs0KUU9MiO8bWsRKNsAk9jknc+bjsre9Lje0sr6jvE840XZSTX/aaXY2g+Mt+9oq+w==} + engines: {node: '>=4'} + + '@commitlint/lint@8.3.6': + resolution: {integrity: sha512-M/tysLho4KdsXJp7J7q/c1WEb3Dh75cm86eb0buci8C/DOIegLq/B3DE/8dhxOzGElUW/iq55MyWttJ/MRwKsg==} + engines: {node: '>=4'} + + '@commitlint/load@21.0.1': + resolution: {integrity: sha512-Btg1q1mKmiihN4W3x0EsPDrJMOQfMa9NIqlzlJyXAfxvsOGdGXOW5p3R3RcSxDCaY7JabY9flIl+Om1af3PSrw==} + engines: {node: '>=22.12.0'} + + '@commitlint/load@8.3.6': + resolution: {integrity: sha512-bqqGg89KnfauJ01GrVBgKyWBXYy2UXmLvRGuepyI1HsNVaEIGBz6R+sTvk3K55Str6soF7HRpl6bDCmnEOVJtA==} + engines: {node: '>=4'} + + '@commitlint/message@8.3.6': + resolution: {integrity: sha512-x30GmsyZTk+QV4o5TRrDkZQm7uRumlKu+7yWeRdSAXyUgi9amsdMFJ8VbAoRsBndOAtEUkaXgK8dvvmgvW3kwg==} + engines: {node: '>=4'} + + '@commitlint/parse@8.3.6': + resolution: {integrity: sha512-wL6Z5hZpT8i/3LMwP/CxTMPMU3v4blAbSA8QGPCruFHFtAV8hIiXvD1CNOhyeeuG29GAapopLgNJjtigzlN3kg==} + engines: {node: '>=4'} + + '@commitlint/read@8.3.6': + resolution: {integrity: sha512-ixlvPQO8AGFjE5U4DBwJIZtzIqmGeZKhpNjjuAyTwWfMURpXjv+/pVvq/AY3LvxHJM64DuQp2WqrbwJU6mXvUQ==} + engines: {node: '>=4'} + + '@commitlint/resolve-extends@21.0.1': + resolution: {integrity: sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg==} + engines: {node: '>=22.12.0'} + + '@commitlint/resolve-extends@8.3.6': + resolution: {integrity: sha512-L0/UOBxc3wiA3gzyE8pN9Yunb6FS/2ZDCjieNH0XAgdF2ac5SHh056QE6aQwP7CSCYNEo2+SXxVZr/WOshsQHg==} + engines: {node: '>=4'} + + '@commitlint/rules@8.3.6': + resolution: {integrity: sha512-NmEAWAW0f5Nda7ZJ11vd73PqOt57GvLc1SOfoUKolCC3lSJACj9SCTbfkQh8cEMlLmDpNqaGaVHH1jMYXMqU3g==} + engines: {node: '>=4'} + + '@commitlint/to-lines@8.3.6': + resolution: {integrity: sha512-4g26G37oh5dABVaRGALdlinjQ/wl8b4HTczLwXLKLM0iHHYFu2A1ZwiVJ8avQk/zThw86/HD6zOgGMNPoamjIQ==} + engines: {node: '>=4'} + + '@commitlint/top-level@8.3.6': + resolution: {integrity: sha512-2XG5NhGgEZaFJChCkSTa6wXWYbJqb9DubC6aRuD/cOeHdYh2OYrXT8z0IorN+gR5+MWqdUtIHhRYtz2Xb75gNg==} + engines: {node: '>=4'} + + '@commitlint/types@21.0.1': + resolution: {integrity: sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg==} + engines: {node: '>=22.12.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@csstools/selector-specificity@2.2.0': + resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.10 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@easy-team/koa-history-api-fallback@1.0.0': + resolution: {integrity: sha512-rnlx7B9GPGsV58rYoiXlRImGznL6GKTWwgQgpx8Hj5ZIHaDwed8SySvNqBMxbd/5fVO9WbwoG5JZXuMqRLlT8A==} + engines: {node: '>=0.8'} + + '@eggjs/router@2.2.0': + resolution: {integrity: sha512-EKnOrZ/pHMFQ1gAj3z0Yn1UnGYH/wSsc2jk06H6RrZp6ty9H6RWyP782e7FT8WL3pAevRY+SAQoH7aPh1SDSVA==} + engines: {node: '>= 8.5.0'} + + '@eggjs/yauzl@2.11.0': + resolution: {integrity: sha512-Jq+k2fCZJ3i3HShb0nxLUiAgq5pwo8JTT1TrH22JoehZQ0Nm2dvByGIja1NYfNyuE4Tx5/Dns5nVsBN/mlC8yg==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint/eslintrc@1.4.1': + resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@hapi/bourne@3.0.0': + resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@hot-loader/react-dom@16.14.0': + resolution: {integrity: sha512-EN9czvcLsMYmSDo5yRKZOAq3ZGRlDpad1gPtX0NdMMomJXcPE3yFSeFzE94X/NjOaiSVimB7LuqPYpkWVaIi4Q==} + peerDependencies: + react: ^16.14.0 + + '@hot-loader/react-dom@17.0.2': + resolution: {integrity: sha512-G2RZrFhsQClS+bdDh/Ojpk3SgocLPUGnvnJDTQYnmKSSwXtU+Yh+8QMs+Ia3zaAvBiOSpIIDSUxuN69cvKqrWg==} + peerDependencies: + react: 17.0.2 + + '@humanwhocodes/config-array@0.10.7': + resolution: {integrity: sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/gitignore-to-minimatch@1.0.2': + resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} + + '@humanwhocodes/object-schema@1.2.1': + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + deprecated: Use @eslint/object-schema instead + + '@hutson/parse-repository-url@3.0.2': + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + + '@icons/material@0.2.4': + resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} + peerDependencies: + react: '*' + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/types@25.5.0': + resolution: {integrity: sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==} + engines: {node: '>= 8.3'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@koa/cors@3.4.3': + resolution: {integrity: sha512-WPXQUaAeAMVaLTEFpoq3T2O1C+FstkjJnDQqy95Ck1UdILajsRhu6mhJ8H2f4NFPRBoCNN+qywTJfq/gGki5mw==} + engines: {node: '>= 8.0.0'} + + '@marionebl/sander@0.6.1': + resolution: {integrity: sha512-7f3zZddAk92G1opoX/glbDO6YbrzmMAJAw0RJAcvunnV7sR4L9llyBUAABptKoF1Jf37UQ1QTJy5p2H4J4rBNA==} + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@mrmlnc/readdir-enhanced@2.2.1': + resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} + engines: {node: '>=4'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@1.1.3': + resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==} + engines: {node: '>= 6'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + + '@sindresorhus/is@0.14.0': + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@szmarczak/http-timer@1.1.2': + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/accepts@1.3.7': + resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} + + '@types/bluebird@3.5.42': + resolution: {integrity: sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/classnames@2.3.4': + resolution: {integrity: sha512-dwmfrMMQb9ujX1uYGvB5ERDlOzBNywnZAZBtOe107/hORWP05ESgU4QyaanZMWYYfd2BzrG78y13/Bju8IQcMQ==} + deprecated: This is a stub types definition. classnames provides its own type definitions, so you do not need this installed. + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/content-disposition@0.5.9': + resolution: {integrity: sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==} + + '@types/continuation-local-storage@3.2.7': + resolution: {integrity: sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==} + + '@types/cookies@0.9.2': + resolution: {integrity: sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/dargs@5.1.0': + resolution: {integrity: sha512-2cXlO8pz13kVYMp6Zgr8Z5DACbaGfoBp7svqZqPGcO+qG3LQLWdB5BzPPASj+UI447XxGmFHi6KjLgUB0fzucQ==} + + '@types/depd@1.1.37': + resolution: {integrity: sha512-PkEYFHnqDFgs+bJXJX0L8mq7sn3DWh+TP0m8BBJUJfZ2WcjRm7jd7Cq68jIJt+c31R1gX0cwSK1ZXOECvN97Rg==} + + '@types/eslint-visitor-keys@1.0.0': + resolution: {integrity: sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/geojson@1.0.6': + resolution: {integrity: sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/html2canvas@0.0.36': + resolution: {integrity: sha512-6g/TVYK5U5AyRaOj1lHVnEmlxzH21K9CRDkC0s6s3CAWLDFbwZ8s+dy0kPrhPfo0cEjIOQh3eeFtTjRBPN4Q0g==} + + '@types/http-assert@1.5.6': + resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/http-proxy@1.17.17': + resolution: {integrity: sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@1.1.2': + resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} + + '@types/js-cookie@2.2.7': + resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/keygrip@1.0.6': + resolution: {integrity: sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/koa-compose@3.2.9': + resolution: {integrity: sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==} + + '@types/koa-router@7.4.9': + resolution: {integrity: sha512-YibkqJrFWwwF55YtZo6dORpMj/1zfweHHIZA/qpA+2JPcVbBopAvLjejTcO1ojWAJfzf1tPC+CrPnLDuVRkUEA==} + + '@types/koa@2.15.2': + resolution: {integrity: sha512-CB+iyjjh1uS5N6/CKwXvw0qA7USMS2WVc4Tjf660yCjhdvqzNr8gdFcIawB41zGGptOQ+d1fnpaQWIIUXYxR3w==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/minimatch@6.0.0': + resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} + deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + + '@types/minimist@1.2.5': + resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} + + '@types/node-ssh@7.0.6': + resolution: {integrity: sha512-XHyzfQ8/PW1C6VEW1f5tZntd6rFyppxXClDtxLvG311vWq46oQWa5ztCOPGNC0KIJekE8G6fd6NqmwIct63XmA==} + + '@types/node@10.17.60': + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/q@1.5.8': + resolution: {integrity: sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-color@3.0.13': + resolution: {integrity: sha512-2c/9FZ4ixC5T3JzN0LP5Cke2Mf0MKOP2Eh0NPDPWmuVH3NjPyhEjqNMQpN1Phr5m74egAy+p2lYNAFrX1z9Yrg==} + peerDependencies: + '@types/react': '*' + + '@types/react-dom@16.9.25': + resolution: {integrity: sha512-ZK//eAPhwft9Ul2/Zj+6O11YR6L4JX0J2sVeBC9Ft7x7HFN7xk7yUV/zDxqV6rjvqgl6r8Dq7oQImxtyf/Mzcw==} + peerDependencies: + '@types/react': ^16.0.0 + + '@types/react-redux@7.1.34': + resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} + + '@types/react-router-config@5.0.11': + resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + + '@types/react-slick@0.23.13': + resolution: {integrity: sha512-bNZfDhe/L8t5OQzIyhrRhBr/61pfBcWaYJoq6UDqFtv5LMwfg4NsVDD2J8N01JqdAdxLjOt66OZEp6PX+dGs/A==} + + '@types/react@16.14.70': + resolution: {integrity: sha512-DM5Q7rSx9G6QYcVvMgxvEurL5P06OxcDNUXrLxlpBzG4ccUewcBCmsztYbxJBobzO8RIwwmjoaD5OsKqdHDuYQ==} + + '@types/reactcss@1.2.13': + resolution: {integrity: sha512-gi3S+aUi6kpkF5vdhUsnkwbiSEIU/BEJyD7kBy2SudWBUuKmJk8AQKE0OVcQQeEy40Azh0lV6uynxlikYIJuwg==} + peerDependencies: + '@types/react': '*' + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/scheduler@0.16.8': + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/sequelize@4.28.20': + resolution: {integrity: sha512-XaGOKRhdizC87hDgQ0u3btxzbejlF+t6Hhvkek1HyphqCI4y7zVBIVAGmuc4cWJqGpxusZ1RiBToHHnNK/Edlw==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@12.0.20': + resolution: {integrity: sha512-MjOKUoDmNattFOBJvAZng7X9KXIKSGy6XHoXY9mASkKwCn35X4Ckh+Ugv1DewXZXrWYXMNtLiXhlCfWlpcAV+Q==} + + '@types/yargs@15.0.20': + resolution: {integrity: sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==} + + '@typescript-eslint/eslint-plugin@2.34.0': + resolution: {integrity: sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + '@typescript-eslint/parser': ^2.0.0 + eslint: ^5.0.0 || ^6.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/eslint-plugin@5.30.0': + resolution: {integrity: sha512-lvhRJ2pGe2V9MEU46ELTdiHgiAFZPKtLhiU5wlnaYpMc2+c1R8fh8i80ZAa665drvjHKUJyRRGg3gEm1If54ow==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/experimental-utils@2.34.0': + resolution: {integrity: sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + eslint: '*' + + '@typescript-eslint/parser@2.34.0': + resolution: {integrity: sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + eslint: ^5.0.0 || ^6.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@5.30.0': + resolution: {integrity: sha512-2oYYUws5o2liX6SrFQ5RB88+PuRymaM2EU02/9Ppoyu70vllPnHVO7ioxDdq/ypXHA277R04SVjxvwI8HmZpzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@5.30.0': + resolution: {integrity: sha512-3TZxvlQcK5fhTBw5solQucWSJvonXf5yua5nx8OqK94hxdrT7/6W3/CS42MLd/f1BmlmmbGEgQcTHHCktUX5bQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/type-utils@5.30.0': + resolution: {integrity: sha512-GF8JZbZqSS+azehzlv/lmQQ3EU3VfWYzCczdZjJRxSEeXDQkqFhCBgFhallLDbPwQOEQ4MHpiPfkjKk7zlmeNg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@5.30.0': + resolution: {integrity: sha512-vfqcBrsRNWw/LBXyncMF/KrUTYYzzygCSsVqlZ1qGu1QtGs6vMkt3US0VNSQ05grXi5Yadp3qv5XZdYLjpp8ag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/typescript-estree@2.34.0': + resolution: {integrity: sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@5.30.0': + resolution: {integrity: sha512-hDEawogreZB4n1zoqcrrtg/wPyyiCxmhPLpZ6kmWfKF5M5G0clRLaEexpuWr31fZ42F96SlD/5xCt1bT5Qm4Nw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@5.30.0': + resolution: {integrity: sha512-0bIgOgZflLKIcZsWvfklsaQTM3ZUbmtH0rJ1hKyV3raoUYyeZwcjQ8ZUJTzS7KnhNcsVT1Rxs7zeeMHEhGlltw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@5.30.0': + resolution: {integrity: sha512-6WcIeRk2DQ3pHKxU1Ni0qMXJkjO/zLjBymlYBy/53qxe7yjEFSvzKLDToJjURUhSl2Fzhkl4SMXQoETauF74cw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@webassemblyjs/ast@1.7.11': + resolution: {integrity: sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==} + + '@webassemblyjs/ast@1.9.0': + resolution: {integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==} + + '@webassemblyjs/floating-point-hex-parser@1.7.11': + resolution: {integrity: sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==} + + '@webassemblyjs/floating-point-hex-parser@1.9.0': + resolution: {integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==} + + '@webassemblyjs/helper-api-error@1.7.11': + resolution: {integrity: sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==} + + '@webassemblyjs/helper-api-error@1.9.0': + resolution: {integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==} + + '@webassemblyjs/helper-buffer@1.7.11': + resolution: {integrity: sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==} + + '@webassemblyjs/helper-buffer@1.9.0': + resolution: {integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==} + + '@webassemblyjs/helper-code-frame@1.7.11': + resolution: {integrity: sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==} + + '@webassemblyjs/helper-code-frame@1.9.0': + resolution: {integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==} + + '@webassemblyjs/helper-fsm@1.7.11': + resolution: {integrity: sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==} + + '@webassemblyjs/helper-fsm@1.9.0': + resolution: {integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==} + + '@webassemblyjs/helper-module-context@1.7.11': + resolution: {integrity: sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==} + + '@webassemblyjs/helper-module-context@1.9.0': + resolution: {integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==} + + '@webassemblyjs/helper-wasm-bytecode@1.7.11': + resolution: {integrity: sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==} + + '@webassemblyjs/helper-wasm-bytecode@1.9.0': + resolution: {integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==} + + '@webassemblyjs/helper-wasm-section@1.7.11': + resolution: {integrity: sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==} + + '@webassemblyjs/helper-wasm-section@1.9.0': + resolution: {integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==} + + '@webassemblyjs/ieee754@1.7.11': + resolution: {integrity: sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==} + + '@webassemblyjs/ieee754@1.9.0': + resolution: {integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==} + + '@webassemblyjs/leb128@1.7.11': + resolution: {integrity: sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==} + + '@webassemblyjs/leb128@1.9.0': + resolution: {integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==} + + '@webassemblyjs/utf8@1.7.11': + resolution: {integrity: sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==} + + '@webassemblyjs/utf8@1.9.0': + resolution: {integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==} + + '@webassemblyjs/wasm-edit@1.7.11': + resolution: {integrity: sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==} + + '@webassemblyjs/wasm-edit@1.9.0': + resolution: {integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==} + + '@webassemblyjs/wasm-gen@1.7.11': + resolution: {integrity: sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==} + + '@webassemblyjs/wasm-gen@1.9.0': + resolution: {integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==} + + '@webassemblyjs/wasm-opt@1.7.11': + resolution: {integrity: sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==} + + '@webassemblyjs/wasm-opt@1.9.0': + resolution: {integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==} + + '@webassemblyjs/wasm-parser@1.7.11': + resolution: {integrity: sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==} + + '@webassemblyjs/wasm-parser@1.9.0': + resolution: {integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==} + + '@webassemblyjs/wast-parser@1.7.11': + resolution: {integrity: sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==} + + '@webassemblyjs/wast-parser@1.9.0': + resolution: {integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==} + + '@webassemblyjs/wast-printer@1.7.11': + resolution: {integrity: sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==} + + '@webassemblyjs/wast-printer@1.9.0': + resolution: {integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.1': + resolution: {integrity: sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + accepts@1.2.13: + resolution: {integrity: sha512-R190A3EzrS4huFOVZajhXCYZt5p5yrkaQOB4nsWzfth0cYaDcSN5J86l58FJ1dt7igp37fB/QhnuFkGAJmr+eg==} + engines: {node: '>= 0.6'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-dynamic-import@3.0.0: + resolution: {integrity: sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==} + deprecated: This is probably built in to whatever tool you're using. If you still need it... idk + + acorn-es7-plugin@1.1.7: + resolution: {integrity: sha512-7D+8kscFMf6F2t+8ZRYmv82CncDZETsaZ4dEl5lh3qQez7FVABk2Vz616SAbnIq1PbNsLVaZjl2oSkk5BWAKng==} + + acorn-globals@4.3.4: + resolution: {integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==} + + acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + + acorn-jsx@3.0.1: + resolution: {integrity: sha512-AU7pnZkguthwBjKgCg6998ByQNIMjbuDQZ8bb78QAFZwPfmKia8AIzgY/gWgqCjnht8JLdXmB4YxA0KaV60ncQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@6.2.0: + resolution: {integrity: sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==} + engines: {node: '>=0.4.0'} + + acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@3.3.0: + resolution: {integrity: sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@5.7.4: + resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@6.4.2: + resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + add-dom-event-listener@1.1.0: + resolution: {integrity: sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==} + + add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + adm-zip@0.5.17: + resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} + engines: {node: '>=12.0'} + + after@0.8.1: + resolution: {integrity: sha512-SuI3vWhCFeSmkmmJ3efyuOkrhGyp/AuHthh3F5DinGYh2kR9t/0xUlm3/Vn2qMScfgg+cKho5fW7TUEYUhYeiA==} + + after@0.8.2: + resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ajv-errors@1.0.1: + resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} + peerDependencies: + ajv: '>=5.0.0' + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@2.1.1: + resolution: {integrity: sha512-ZFztHzVRdGLAzJmpUT9LNFLe1YiVOEylcaNpEutM26PVTCtOD919IMfD01CgbRouB42Dd9atjx1HseC15DgOZA==} + peerDependencies: + ajv: ^5.0.0 + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@5.5.2: + resolution: {integrity: sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + alphanum-sort@1.0.2: + resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==} + + amdefine@1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + + ansi-align@2.0.0: + resolution: {integrity: sha512-TdlOggdA/zURfMYa7ABC66j+oqfMew58KpJMbUlH3bcZP1b+cBHIHDDn5uH9INsxrHBPjsqM0tDB4jPTF/vgJA==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@3.2.3: + resolution: {integrity: sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==} + engines: {node: '>=6'} + + ansi-colors@3.2.4: + resolution: {integrity: sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==} + engines: {node: '>=6'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@2.2.1: + resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==} + engines: {node: '>=0.10.0'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ant-design-dtinsight-theme@1.1.3: + resolution: {integrity: sha512-1lovQrliD6JIIsB5jO5zPq5VLb01ZH/brj2CJVBRmIs9kqbdo01Ln2nGysKLU8Lt0h8OKaieEH63WV15kQwOoQ==} + + antd@3.26.13: + resolution: {integrity: sha512-7DjaXAUig51kzVw9T9Mi4slmq0eFl6qGk7t3kjA5t3Sv/Yn2llwNWT0lJDbseooesRRWeFLNByfZV37cUCRJYQ==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + antd@4.15.6: + resolution: {integrity: sha512-uXn1uRFlPLrAmjfkyxKc3avMVO5N30o3YoSMSJPRw5OLNjOfWsnZDPKvZwinn+QQE9N7dRXylcMfNvl11LH2gA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@1.2.0: + resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} + + archive-tool@1.0.4: + resolution: {integrity: sha512-Zai5QxKt+ZnOynm64TGU4bW3zgRS+efkvcIbdO3PNiGon8ZgbwKtoW3SQUwAvg7TYOdc5oI2hYCixRnX0dE8Sw==} + engines: {node: '>=8.6.0'} + + archiver-utils@1.3.0: + resolution: {integrity: sha512-h+hTREBXcW5e1L9RihGXdH4PHHdGipG/jE2sMZrqIH6BmZAxeGU5IWjVsKhokdCSWX7km6Kkh406zZNEElHFPQ==} + engines: {node: '>= 0.10.0'} + + archiver@2.1.1: + resolution: {integrity: sha512-01psM0DMD3YItvhnAXZODfsViaeDidrJwfne3lsoVrbyYa/xFQwTbVjY+2WlEBm7qH1fCsyxAA1SgNr/XenTlQ==} + engines: {node: '>= 4'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@4.2.2: + resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==} + engines: {node: '>=6.0'} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-differ@1.0.0: + resolution: {integrity: sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==} + engines: {node: '>=0.10.0'} + + array-equal@1.0.2: + resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} + + array-filter@1.0.0: + resolution: {integrity: sha512-Ene1hbrinPZ1qPoZp7NSx4jQnh4nr7MtY78pHNb+yr8yHbxmTS7ChGW0a55JKA7TkRDeoQxK4GcJaCvBYplSKA==} + + array-find-index@1.0.2: + resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} + engines: {node: '>=0.10.0'} + + array-find@1.0.0: + resolution: {integrity: sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-tree-filter@2.1.0: + resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==} + + array-union@1.0.2: + resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} + engines: {node: '>=0.10.0'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array-uniq@1.0.3: + resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} + engines: {node: '>=0.10.0'} + + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.reduce@1.0.8: + resolution: {integrity: sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + arraybuffer.slice@0.0.6: + resolution: {integrity: sha512-6ZjfQaBSy6CuIH0+B0NrxMfDE5VIOCP/5gOqSpEIsaAZx9/giszzrXg6PZ7G51U/n88UmlAgYLNQ9wAnII7PJA==} + + arraybuffer.slice@0.0.7: + resolution: {integrity: sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + art-template@4.13.4: + resolution: {integrity: sha512-SoCdwWrj6VNoErheIKnDSzBEO0SsZJK9p34nliZUuChkfiReu8x4hLP/jBTLG6yEr+tWxdANufophDYsau52Fg==} + engines: {node: '>= 1.0.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1.js@4.10.1: + resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assert@1.5.1: + resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + ast-types-flow@0.0.7: + resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-each@1.0.6: + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + + async-validator@1.11.5: + resolution: {integrity: sha512-XNtCsMAeAH1pdLMEg1z8/Bb3a8cdCbui9QbJATRFHHHW5kT6+NPI3zSVQUXgikTFITzsg+kYY5NTWhM2Orwt9w==} + + async-validator@3.5.2: + resolution: {integrity: sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==} + + async@1.5.2: + resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + autoprefixer@9.8.8: + resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==} + hasBin: true + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + await-event@2.1.0: + resolution: {integrity: sha512-hADm2dFnyugZnfFoJ0Oug2T9xAT2gFdvxZXXnWUOFsHL+VTCvj4Q7oBOinUYzvAFeAD5HN1YSrP78iS3/SQ7iQ==} + + await-first@1.0.0: + resolution: {integrity: sha512-SK20HicVu6lXvNM0nS1flurrs4/1NdhvccvEn52Gf+vpERZnnkKBnJvAQDsYkzJnsHs1bRNNKEiobEet7a/0TA==} + engines: {node: '>= 6.0.0'} + + await-stream-ready@1.0.1: + resolution: {integrity: sha512-6ixSbvbBLztuO5BVFjb2N+8ol6W+/2hEGdmhIGYUPY8IsVU4Rg84WRoLRQRtz0nbhkx1Jnu7S7XiVAzW3lLF/g==} + engines: {node: '>=4.0.0'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + axe-core@4.11.4: + resolution: {integrity: sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==} + engines: {node: '>=4'} + + axios@0.19.2: + resolution: {integrity: sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==} + deprecated: Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410 + + axobject-query@2.2.0: + resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + babel-code-frame@6.26.0: + resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} + + babel-core@6.26.3: + resolution: {integrity: sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==} + + babel-eslint@10.1.0: + resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} + engines: {node: '>=6'} + deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. + peerDependencies: + eslint: '>= 4.12.1' + + babel-eslint@7.2.3: + resolution: {integrity: sha512-i2yKOhjgwUbUrJ8oJm6QqRzltIoFahGNPZ0HF22lUN4H1DW03JQyJm7WSv+I1LURQWjDNhVqFo04acYa07rhOQ==} + engines: {node: '>=4'} + deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. + + babel-eslint@8.2.6: + resolution: {integrity: sha512-aCdHjhzcILdP8c9lej7hvXKvQieyRt20SF102SIGyY4cUIiw6UaAtK4j2o3dXX74jEmy0TJ0CEhv4fTIM3SzcA==} + engines: {node: '>=4'} + deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. + + babel-generator@6.26.1: + resolution: {integrity: sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==} + + babel-helper-bindify-decorators@6.24.1: + resolution: {integrity: sha512-TYX2QQATKA6Wssp6j7jqlw4QLmABDN1olRdEHndYvBXdaXM5dcx6j5rN0+nd+aVL+Th40fAEYvvw/Xxd/LETuQ==} + + babel-helper-builder-binary-assignment-operator-visitor@6.24.1: + resolution: {integrity: sha512-gCtfYORSG1fUMX4kKraymq607FWgMWg+j42IFPc18kFQEsmtaibP4UrqsXt8FlEJle25HUd4tsoDR7H2wDhe9Q==} + + babel-helper-builder-react-jsx@6.26.0: + resolution: {integrity: sha512-02I9jDjnVEuGy2BR3LRm9nPRb/+Ja0pvZVLr1eI5TYAA/dB0Xoc+WBo50+aDfhGDLhlBY1+QURjn9uvcFd8gzg==} + + babel-helper-call-delegate@6.24.1: + resolution: {integrity: sha512-RL8n2NiEj+kKztlrVJM9JT1cXzzAdvWFh76xh/H1I4nKwunzE4INBXn8ieCZ+wh4zWszZk7NBS1s/8HR5jDkzQ==} + + babel-helper-define-map@6.26.0: + resolution: {integrity: sha512-bHkmjcC9lM1kmZcVpA5t2om2nzT/xiZpo6TJq7UlZ3wqKfzia4veeXbIhKvJXAMzhhEBd3cR1IElL5AenWEUpA==} + + babel-helper-explode-assignable-expression@6.24.1: + resolution: {integrity: sha512-qe5csbhbvq6ccry9G7tkXbzNtcDiH4r51rrPUbwwoTzZ18AqxWYRZT6AOmxrpxKnQBW0pYlBI/8vh73Z//78nQ==} + + babel-helper-explode-class@6.24.1: + resolution: {integrity: sha512-SFbWewr0/0U4AiRzsHqwsbOQeLXVa9T1ELdqEa2efcQB5KopTnunAqoj07TuHlN2lfTQNPGO/rJR4FMln5fVcA==} + + babel-helper-function-name@6.24.1: + resolution: {integrity: sha512-Oo6+e2iX+o9eVvJ9Y5eKL5iryeRdsIkwRYheCuhYdVHsdEQysbc2z2QkqCLIYnNxkT5Ss3ggrHdXiDI7Dhrn4Q==} + + babel-helper-get-function-arity@6.24.1: + resolution: {integrity: sha512-WfgKFX6swFB1jS2vo+DwivRN4NB8XUdM3ij0Y1gnC21y1tdBoe6xjVnd7NSI6alv+gZXCtJqvrTeMW3fR/c0ng==} + + babel-helper-hoist-variables@6.24.1: + resolution: {integrity: sha512-zAYl3tqerLItvG5cKYw7f1SpvIxS9zi7ohyGHaI9cgDUjAT6YcY9jIEH5CstetP5wHIVSceXwNS7Z5BpJg+rOw==} + + babel-helper-optimise-call-expression@6.24.1: + resolution: {integrity: sha512-Op9IhEaxhbRT8MDXx2iNuMgciu2V8lDvYCNQbDGjdBNCjaMvyLf4wl4A3b8IgndCyQF8TwfgsQ8T3VD8aX1/pA==} + + babel-helper-regex@6.26.0: + resolution: {integrity: sha512-VlPiWmqmGJp0x0oK27Out1D+71nVVCTSdlbhIVoaBAj2lUgrNjBCRR9+llO4lTSb2O4r7PJg+RobRkhBrf6ofg==} + + babel-helper-remap-async-to-generator@6.24.1: + resolution: {integrity: sha512-RYqaPD0mQyQIFRu7Ho5wE2yvA/5jxqCIj/Lv4BXNq23mHYu/vxikOy2JueLiBxQknwapwrJeNCesvY0ZcfnlHg==} + + babel-helper-replace-supers@6.24.1: + resolution: {integrity: sha512-sLI+u7sXJh6+ToqDr57Bv973kCepItDhMou0xCP2YPVmR1jkHSCY+p1no8xErbV1Siz5QE8qKT1WIwybSWlqjw==} + + babel-helpers@6.24.1: + resolution: {integrity: sha512-n7pFrqQm44TCYvrCDb0MqabAF+JUBq+ijBvNMUxpkLjJaAu32faIexewMumrH5KLLJ1HDyT0PTEqRyAe/GwwuQ==} + + babel-loader@7.1.5: + resolution: {integrity: sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw==} + engines: {node: '>=4'} + peerDependencies: + babel-core: '6' + webpack: 2 || 3 || 4 + + babel-messages@6.23.0: + resolution: {integrity: sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==} + + babel-plugin-add-module-exports@0.2.1: + resolution: {integrity: sha512-3AN/9V/rKuv90NG65m4tTHsI04XrCKsWbztIcW7a8H5iIN7WlvWucRtVV0V/rT4QvtA11n5Vmp20fLwfMWqp6g==} + + babel-plugin-check-es2015-constants@6.22.0: + resolution: {integrity: sha512-B1M5KBP29248dViEo1owyY32lk1ZSH2DaNNrXLGt8lyjjHm7pBqAdQ7VKUPR6EEDO323+OvT3MQXbCin8ooWdA==} + + babel-plugin-import@1.13.8: + resolution: {integrity: sha512-36babpjra5m3gca44V6tSTomeBlPA7cHUynrE2WiQIm3rEGD9xy28MKsx5IdO45EbnpJY7Jrgd00C6Dwt/l/2Q==} + + babel-plugin-syntax-async-functions@6.13.0: + resolution: {integrity: sha512-4Zp4unmHgw30A1eWI5EpACji2qMocisdXhAftfhXoSV9j0Tvj6nRFE3tOmRY912E0FMRm/L5xWE7MGVT2FoLnw==} + + babel-plugin-syntax-async-generators@6.13.0: + resolution: {integrity: sha512-EbciFN5Jb9iqU9bqaLmmFLx2G8pAUsvpWJ6OzOWBNrSY9qTohXj+7YfZx6Ug1Qqh7tCb1EA7Jvn9bMC1HBiucg==} + + babel-plugin-syntax-class-constructor-call@6.18.0: + resolution: {integrity: sha512-EEuBcXz/wZ81Jaac0LnMHtD4Mfz9XWn2oH2Xj+CHwz2SZWUqqdtR2BgWPSdTGMmxN/5KLSh4PImt9+9ZedDarA==} + + babel-plugin-syntax-class-properties@6.13.0: + resolution: {integrity: sha512-chI3Rt9T1AbrQD1s+vxw3KcwC9yHtF621/MacuItITfZX344uhQoANjpoSJZleAmW2tjlolqB/f+h7jIqXa7pA==} + + babel-plugin-syntax-decorators@6.13.0: + resolution: {integrity: sha512-AWj19x2aDm8qFQ5O2JcD6pwJDW1YdcnO+1b81t7gxrGjz5VHiUqeYWAR4h7zueWMalRelrQDXprv2FrY1dbpbw==} + + babel-plugin-syntax-do-expressions@6.13.0: + resolution: {integrity: sha512-HD/5qJB9oSXzl0caxM+aRD7ENICXqcc3Up/8toDQk7zNIDE7TzsqtxC5f4t9Rwhu2Ya8l9l4j6b3vOsy+a6qxg==} + + babel-plugin-syntax-dynamic-import@6.18.0: + resolution: {integrity: sha512-MioUE+LfjCEz65Wf7Z/Rm4XCP5k2c+TbMd2Z2JKc7U9uwjBhAfNPE48KC4GTGKhppMeYVepwDBNO/nGY6NYHBA==} + + babel-plugin-syntax-exponentiation-operator@6.13.0: + resolution: {integrity: sha512-Z/flU+T9ta0aIEKl1tGEmN/pZiI1uXmCiGFRegKacQfEJzp7iNsKloZmyJlQr+75FCJtiFfGIK03SiCvCt9cPQ==} + + babel-plugin-syntax-export-extensions@6.13.0: + resolution: {integrity: sha512-Eo0rcRaIDMld/W6mVhePiudIuLW+Cr/8eveW3mBREfZORScZgx4rh6BAPyvzdEc/JZvQ+LkC80t0VGFs6FX+lg==} + + babel-plugin-syntax-flow@6.18.0: + resolution: {integrity: sha512-HbTDIoG1A1op7Tl/wIFQPULIBA61tsJ8Ntq2FAhLwuijrzosM/92kAfgU1Q3Kc7DH/cprJg5vDfuTY4QUL4rDA==} + + babel-plugin-syntax-function-bind@6.13.0: + resolution: {integrity: sha512-m8yMoh9LIiNyeLdQs5I9G+3YXo4nqVsKQkk7YplrG4qAFbNi9hkZlow8HDHxhH9QOVFPHmy8+03NzRCdyChIKw==} + + babel-plugin-syntax-jsx@6.18.0: + resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} + + babel-plugin-syntax-object-rest-spread@6.13.0: + resolution: {integrity: sha512-C4Aq+GaAj83pRQ0EFgTvw5YO6T3Qz2KGrNRwIj9mSoNHVvdZY4KO2uA6HNtNXCw993iSZnckY1aLW8nOi8i4+w==} + + babel-plugin-syntax-trailing-function-commas@6.22.0: + resolution: {integrity: sha512-Gx9CH3Q/3GKbhs07Bszw5fPTlU+ygrOGfAhEt7W2JICwufpC4SuO0mG0+4NykPBSYPMJhqvVlDBU17qB1D+hMQ==} + + babel-plugin-transform-async-generator-functions@6.24.1: + resolution: {integrity: sha512-uT7eovUxtXe8Q2ufcjRuJIOL0hg6VAUJhiWJBLxH/evYAw+aqoJLcYTR8hqx13iOx/FfbCMHgBmXWZjukbkyPg==} + + babel-plugin-transform-async-to-generator@6.24.1: + resolution: {integrity: sha512-7BgYJujNCg0Ti3x0c/DL3tStvnKS6ktIYOmo9wginv/dfZOrbSZ+qG4IRRHMBOzZ5Awb1skTiAsQXg/+IWkZYw==} + + babel-plugin-transform-class-constructor-call@6.24.1: + resolution: {integrity: sha512-RvYukT1Nh7njz8P8326ztpQUGCKwmjgu6aRIx1lkvylWITYcskg29vy1Kp8WXIq7FvhXsz0Crf2kS94bjB690A==} + + babel-plugin-transform-class-properties@6.24.1: + resolution: {integrity: sha512-n4jtBA3OYBdvG5PRMKsMXJXHfLYw/ZOmtxCLOOwz6Ro5XlrColkStLnz1AS1L2yfPA9BKJ1ZNlmVCLjAL9DSIg==} + + babel-plugin-transform-decorators-legacy@1.3.5: + resolution: {integrity: sha512-jYHwjzRXRelYQ1uGm353zNzf3QmtdCfvJbuYTZ4gKveK7M9H1fs3a5AKdY1JUDl0z97E30ukORW1dzhWvsabtA==} + + babel-plugin-transform-decorators@6.24.1: + resolution: {integrity: sha512-skQ2CImwDkCHu0mkWvCOlBCpBIHW4/49IZWVwV4A/EnWjL9bB6UBvLyMNe3Td5XDStSZNhe69j4bfEW8dvUbew==} + + babel-plugin-transform-do-expressions@6.22.0: + resolution: {integrity: sha512-yQwYqYg+Tnj1InA8W1rsItsZVhkv1Euc4KVua9ledtPz5PDWYz7LVyy6rDBpVYUWFZj5k6GUm3YZpCbIm8Tqew==} + + babel-plugin-transform-es2015-arrow-functions@6.22.0: + resolution: {integrity: sha512-PCqwwzODXW7JMrzu+yZIaYbPQSKjDTAsNNlK2l5Gg9g4rz2VzLnZsStvp/3c46GfXpwkyufb3NCyG9+50FF1Vg==} + + babel-plugin-transform-es2015-block-scoped-functions@6.22.0: + resolution: {integrity: sha512-2+ujAT2UMBzYFm7tidUsYh+ZoIutxJ3pN9IYrF1/H6dCKtECfhmB8UkHVpyxDwkj0CYbQG35ykoz925TUnBc3A==} + + babel-plugin-transform-es2015-block-scoping@6.26.0: + resolution: {integrity: sha512-YiN6sFAQ5lML8JjCmr7uerS5Yc/EMbgg9G8ZNmk2E3nYX4ckHR01wrkeeMijEf5WHNK5TW0Sl0Uu3pv3EdOJWw==} + + babel-plugin-transform-es2015-classes@6.24.1: + resolution: {integrity: sha512-5Dy7ZbRinGrNtmWpquZKZ3EGY8sDgIVB4CU8Om8q8tnMLrD/m94cKglVcHps0BCTdZ0TJeeAWOq2TK9MIY6cag==} + + babel-plugin-transform-es2015-computed-properties@6.24.1: + resolution: {integrity: sha512-C/uAv4ktFP/Hmh01gMTvYvICrKze0XVX9f2PdIXuriCSvUmV9j+u+BB9f5fJK3+878yMK6dkdcq+Ymr9mrcLzw==} + + babel-plugin-transform-es2015-destructuring@6.23.0: + resolution: {integrity: sha512-aNv/GDAW0j/f4Uy1OEPZn1mqD+Nfy9viFGBfQ5bZyT35YqOiqx7/tXdyfZkJ1sC21NyEsBdfDY6PYmLHF4r5iA==} + + babel-plugin-transform-es2015-duplicate-keys@6.24.1: + resolution: {integrity: sha512-ossocTuPOssfxO2h+Z3/Ea1Vo1wWx31Uqy9vIiJusOP4TbF7tPs9U0sJ9pX9OJPf4lXRGj5+6Gkl/HHKiAP5ug==} + + babel-plugin-transform-es2015-for-of@6.23.0: + resolution: {integrity: sha512-DLuRwoygCoXx+YfxHLkVx5/NpeSbVwfoTeBykpJK7JhYWlL/O8hgAK/reforUnZDlxasOrVPPJVI/guE3dCwkw==} + + babel-plugin-transform-es2015-function-name@6.24.1: + resolution: {integrity: sha512-iFp5KIcorf11iBqu/y/a7DK3MN5di3pNCzto61FqCNnUX4qeBwcV1SLqe10oXNnCaxBUImX3SckX2/o1nsrTcg==} + + babel-plugin-transform-es2015-literals@6.22.0: + resolution: {integrity: sha512-tjFl0cwMPpDYyoqYA9li1/7mGFit39XiNX5DKC/uCNjBctMxyL1/PT/l4rSlbvBG1pOKI88STRdUsWXB3/Q9hQ==} + + babel-plugin-transform-es2015-modules-amd@6.24.1: + resolution: {integrity: sha512-LnIIdGWIKdw7zwckqx+eGjcS8/cl8D74A3BpJbGjKTFFNJSMrjN4bIh22HY1AlkUbeLG6X6OZj56BDvWD+OeFA==} + + babel-plugin-transform-es2015-modules-commonjs@6.26.2: + resolution: {integrity: sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==} + + babel-plugin-transform-es2015-modules-systemjs@6.24.1: + resolution: {integrity: sha512-ONFIPsq8y4bls5PPsAWYXH/21Hqv64TBxdje0FvU3MhIV6QM2j5YS7KvAzg/nTIVLot2D2fmFQrFWCbgHlFEjg==} + + babel-plugin-transform-es2015-modules-umd@6.24.1: + resolution: {integrity: sha512-LpVbiT9CLsuAIp3IG0tfbVo81QIhn6pE8xBJ7XSeCtFlMltuar5VuBV6y6Q45tpui9QWcy5i0vLQfCfrnF7Kiw==} + + babel-plugin-transform-es2015-object-super@6.24.1: + resolution: {integrity: sha512-8G5hpZMecb53vpD3mjs64NhI1au24TAmokQ4B+TBFBjN9cVoGoOvotdrMMRmHvVZUEvqGUPWL514woru1ChZMA==} + + babel-plugin-transform-es2015-parameters@6.24.1: + resolution: {integrity: sha512-8HxlW+BB5HqniD+nLkQ4xSAVq3bR/pcYW9IigY+2y0dI+Y7INFeTbfAQr+63T3E4UDsZGjyb+l9txUnABWxlOQ==} + + babel-plugin-transform-es2015-shorthand-properties@6.24.1: + resolution: {integrity: sha512-mDdocSfUVm1/7Jw/FIRNw9vPrBQNePy6wZJlR8HAUBLybNp1w/6lr6zZ2pjMShee65t/ybR5pT8ulkLzD1xwiw==} + + babel-plugin-transform-es2015-spread@6.22.0: + resolution: {integrity: sha512-3Ghhi26r4l3d0Js933E5+IhHwk0A1yiutj9gwvzmFbVV0sPMYk2lekhOufHBswX7NCoSeF4Xrl3sCIuSIa+zOg==} + + babel-plugin-transform-es2015-sticky-regex@6.24.1: + resolution: {integrity: sha512-CYP359ADryTo3pCsH0oxRo/0yn6UsEZLqYohHmvLQdfS9xkf+MbCzE3/Kolw9OYIY4ZMilH25z/5CbQbwDD+lQ==} + + babel-plugin-transform-es2015-template-literals@6.22.0: + resolution: {integrity: sha512-x8b9W0ngnKzDMHimVtTfn5ryimars1ByTqsfBDwAqLibmuuQY6pgBQi5z1ErIsUOWBdw1bW9FSz5RZUojM4apg==} + + babel-plugin-transform-es2015-typeof-symbol@6.23.0: + resolution: {integrity: sha512-fz6J2Sf4gYN6gWgRZaoFXmq93X+Li/8vf+fb0sGDVtdeWvxC9y5/bTD7bvfWMEq6zetGEHpWjtzRGSugt5kNqw==} + + babel-plugin-transform-es2015-unicode-regex@6.24.1: + resolution: {integrity: sha512-v61Dbbihf5XxnYjtBN04B/JBvsScY37R1cZT5r9permN1cp+b70DY3Ib3fIkgn1DI9U3tGgBJZVD8p/mE/4JbQ==} + + babel-plugin-transform-exponentiation-operator@6.24.1: + resolution: {integrity: sha512-LzXDmbMkklvNhprr20//RStKVcT8Cu+SQtX18eMHLhjHf2yFzwtQ0S2f0jQ+89rokoNdmwoSqYzAhq86FxlLSQ==} + + babel-plugin-transform-export-extensions@6.22.0: + resolution: {integrity: sha512-mtzELzINaYqdVglyZrDDVwkcFRuE7s6QUFWXxwffKAHB/NkfbJ2NJSytugB43ytIC8UVt30Ereyx+7gNyTkDLg==} + + babel-plugin-transform-flow-strip-types@6.22.0: + resolution: {integrity: sha512-TxIM0ZWNw9oYsoTthL3lvAK3+eTujzktoXJg4ubGvICGbVuXVYv5hHv0XXpz8fbqlJaGYY4q5SVzaSmsg3t4Fg==} + + babel-plugin-transform-function-bind@6.22.0: + resolution: {integrity: sha512-9Ec4KYf1GurT39mlUjDSlN7HWSlB3u3mWRMogQbb+Y88lO0ZM3rJ0ADhPnQwWK9TbO6e/4E+Et1rrfGY9mFimA==} + + babel-plugin-transform-object-assign@6.22.0: + resolution: {integrity: sha512-N6Pddn/0vgLjnGr+mS7ttlFkQthqcnINE9EMOxB0CF8F4t6kuJXz6NUeLfSoRbLmkGh0mgDs9i2isdaZj0Ghtg==} + + babel-plugin-transform-object-rest-spread@6.26.0: + resolution: {integrity: sha512-ocgA9VJvyxwt+qJB0ncxV8kb/CjfTcECUY4tQ5VT7nP6Aohzobm8CDFaQ5FHdvZQzLmf0sgDxB8iRXZXxwZcyA==} + + babel-plugin-transform-react-display-name@6.25.0: + resolution: {integrity: sha512-QLYkLiZeeED2PKd4LuXGg5y9fCgPB5ohF8olWUuETE2ryHNRqqnXlEVP7RPuef89+HTfd3syptMGVHeoAu0Wig==} + + babel-plugin-transform-react-jsx-self@6.22.0: + resolution: {integrity: sha512-Y3ZHP1nunv0U1+ysTNwLK39pabHj6cPVsfN4TRC7BDBfbgbyF4RifP5kd6LnbuMV9wcfedQMe7hn1fyKc7IzTQ==} + + babel-plugin-transform-react-jsx-source@6.22.0: + resolution: {integrity: sha512-pcDNDsZ9q/6LJmujQ/OhjeoIlp5Nl546HJ2yiFIJK3mYpgNXhI5/S9mXfVxu5yqWAi7HdI7e/q6a9xtzwL69Vw==} + + babel-plugin-transform-react-jsx@6.24.1: + resolution: {integrity: sha512-s+q/Y2u2OgDPHRuod3t6zyLoV8pUHc64i/O7ZNgIOEdYTq+ChPeybcKBi/xk9VI60VriILzFPW+dUxAEbTxh2w==} + + babel-plugin-transform-regenerator@6.26.0: + resolution: {integrity: sha512-LS+dBkUGlNR15/5WHKe/8Neawx663qttS6AGqoOUhICc9d1KciBvtrQSuc0PI+CxQ2Q/S1aKuJ+u64GtLdcEZg==} + + babel-plugin-transform-runtime@6.23.0: + resolution: {integrity: sha512-cpGMVC1vt/772y3jx1gwSaTitQVZuFDlllgreMsZ+rTYC6jlYXRyf5FQOgSnckOiA5QmzbXTyBY2A5AmZXF1fA==} + + babel-plugin-transform-strict-mode@6.24.1: + resolution: {integrity: sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==} + + babel-polyfill@6.26.0: + resolution: {integrity: sha512-F2rZGQnAdaHWQ8YAoeRbukc7HS9QgdgeyJ0rQDd485v9opwuPvjpPFcOOT/WmkKTdgy9ESgSPXDcTNpzrGr6iQ==} + + babel-preset-env@1.7.0: + resolution: {integrity: sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==} + + babel-preset-flow@6.23.0: + resolution: {integrity: sha512-PQZFJXnM3d80Vq4O67OE6EMVKIw2Vmzy8UXovqulNogCtblWU8rzP7Sm5YgHiCg4uejUxzCkHfNXQ4Z6GI+Dhw==} + + babel-preset-react@6.24.1: + resolution: {integrity: sha512-phQe3bElbgF887UM0Dhz55d22ob8czTL1kbhZFwpCE6+R/X9kHktfwmx9JZb+bBSVRGphP5tZ9oWhVhlgjrX3Q==} + + babel-preset-stage-0@6.24.1: + resolution: {integrity: sha512-MJD+xBbpsApbKlzAX0sOBF+VeFaUmv5s8FSOO7SSZpes1QgphCjq/UIGRFWSmQ/0i5bqQjLGCTXGGXqcLQ9JDA==} + + babel-preset-stage-1@6.24.1: + resolution: {integrity: sha512-rn+UOcd7BHDniq1SVxv2/AVVSVI1NK+hfS0I/iR6m6KbOi/aeBRcqBilqO73pd9VUpRXF2HFtlDuC9F2BEQqmg==} + + babel-preset-stage-2@6.24.1: + resolution: {integrity: sha512-9F+nquz+37PrlTSBdpeQBKnQfAMNBnryXw+m4qBh35FNbJPfzZz+sjN2G5Uf1CRedU9PH7fJkTbYijxmkLX8Og==} + + babel-preset-stage-3@6.24.1: + resolution: {integrity: sha512-eCbEOF8uN0KypFXJmZXn2sTk7bPV9uM5xov7G/7BM08TbQEObsVs0cEWfy6NQySlfk7JBi/t+XJP1JkruYfthA==} + + babel-register@6.26.0: + resolution: {integrity: sha512-veliHlHX06wjaeY8xNITbveXSiI+ASFnOqvne/LaIJIqOWi2Ogmj91KOugEz/hoh/fwMhXNBJPCv8Xaz5CyM4A==} + + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + + babel-template@6.26.0: + resolution: {integrity: sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==} + + babel-traverse@6.26.0: + resolution: {integrity: sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==} + + babel-types@6.26.0: + resolution: {integrity: sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==} + + babel-upgrade@1.0.1: + resolution: {integrity: sha512-1+aRupa4DvXXxVYxC2ejWnul/7Pi1T3hYUmnq4gxgkV/NGJMWpKdtipv7fb0cdS3ifdvryIboAi1fLHLZjHvbg==} + engines: {node: '>=8.0.0'} + hasBin: true + + babylon@6.18.0: + resolution: {integrity: sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==} + hasBin: true + + babylon@7.0.0-beta.44: + resolution: {integrity: sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==} + engines: {node: '>=4.2.0'} + hasBin: true + + backo2@1.0.2: + resolution: {integrity: sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==} + + bail@1.0.5: + resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + base64-arraybuffer@0.1.4: + resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==} + engines: {node: '>= 0.6.0'} + + base64-arraybuffer@0.1.5: + resolution: {integrity: sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64-url@1.2.1: + resolution: {integrity: sha512-V8E0l1jyyeSSS9R+J9oljx5eq2rqzClInuwaPcyuv0Mm3ViI/3/rcc4rCEO8i4eQ4I0O0FAGYDA2i5xWHHPhzg==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + basic-auth-connect@1.0.0: + resolution: {integrity: sha512-kiV+/DTgVro4aZifY/hwRwALBISViL5NP4aReaR2EVJEObpbUBHIkdJh/YpcoEiYt7nBodZ6U2ajZeZvSxUCCg==} + + basic-auth@1.0.4: + resolution: {integrity: sha512-uvq3I/zC5TmG0WZJDzsXzIytU9GiiSq23Gl27Dq9sV81JTfPfQhtdADECP1DJZeJoZPuYU0Y81hWC5y/dOR+Yw==} + engines: {node: '>= 0.6'} + + batch@0.5.3: + resolution: {integrity: sha512-aQgHPLH2DHpFTpBl5/GiVdNzHEqsLCSs1RiPvqkKP1+7RkNJlv71kL8/KXmvvaLqoZ7ylmvqkZhLjjAoRz8Xgw==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + better-assert@1.0.2: + resolution: {integrity: sha512-bYeph2DFlpK1XmGs6fvlLRUN29QISM3GBuUwSFsMY2XRx4AvC0WNCS57j4c/xGrK2RS24C1w3YoBOsw9fT46tQ==} + + bfj@6.1.2: + resolution: {integrity: sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==} + engines: {node: '>= 6.0.0'} + + big.js@3.2.0: + resolution: {integrity: sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@1.13.1: + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@1.2.3: + resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + black-hole-stream@0.0.1: + resolution: {integrity: sha512-FQSWhFQZmddoqWkwPMFeR5hJo9waZE796MuO7b0poStaPrm0Qr2em9FT4/TF1+0rOA1dRFabX+88rdX8YHxDTA==} + + blob@0.0.4: + resolution: {integrity: sha512-YRc9zvVz4wNaxcXmiSgb9LAg7YYwqQ2xd0Sj6osfA7k/PKmIGVlnOYs3wOFdkRC9/JpQu8sGt/zHgJV7xzerfg==} + + blob@0.0.5: + resolution: {integrity: sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==} + + block-stream@0.0.9: + resolution: {integrity: sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==} + engines: {node: 0.4 || >=0.5.8} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + bn.js@4.12.3: + resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} + + bn.js@5.2.3: + resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} + + body-parser@1.13.3: + resolution: {integrity: sha512-ypX8/9uws2W+CjPp3QMmz1qklzlhRBknQve22Y+WFecHql+qDFfG+VVNX7sooA4Q3+2fdq4ZZj6Xr07gA90RZg==} + engines: {node: '>= 0.8'} + + body-parser@1.20.5: + resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@1.3.0: + resolution: {integrity: sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==} + engines: {node: '>=4'} + + boxen@4.2.0: + resolution: {integrity: sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==} + engines: {node: '>=8'} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + + browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + + browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + + browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + + browserify-rsa@4.1.1: + resolution: {integrity: sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==} + engines: {node: '>= 0.10'} + + browserify-sign@4.2.5: + resolution: {integrity: sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==} + engines: {node: '>= 0.10'} + + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + + browserslist@3.2.8: + resolution: {integrity: sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==} + hasBin: true + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + + buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + + builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + + builtins@5.1.0: + resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==} + + busboy@0.2.14: + resolution: {integrity: sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==} + engines: {node: '>=0.8.0'} + + byte@2.0.0: + resolution: {integrity: sha512-rNiK8YxOMvquToaBubKxA10sjRIZ/taDqtc/1jLQA4X7aNDlA1XGx4Ciml3YxL8DskFz1XX3WFskSp0peKYSKg==} + engines: {node: '>= 8.0.0'} + + bytes@2.1.0: + resolution: {integrity: sha512-k9VSlRfRi5JYyQWMylSOgjld96ta1qaQUIvmn+na0BzViclH04PBumewv4z5aeXNkn6Z/gAN5FtPeBLvV20F9w==} + + bytes@2.2.0: + resolution: {integrity: sha512-zGRpnr2l5w/s8PxkrquUJoVeR06KvqPelrYqiSyQV7QEBqCYivpb6UzXYWC6JDBVtNFOT0rzJRFhkfJgxzmILA==} + + bytes@2.4.0: + resolution: {integrity: sha512-SvUX8+c/Ga454a4fprIdIUzUN9xfd1YTvYh7ub5ZPJ+ZJ/+K2Bp6IpWGmnw8r3caLTsmhvJAKZz3qjIo9+XuCQ==} + + bytes@2.5.0: + resolution: {integrity: sha512-hkQtlCqf2f67v+GDlR9DImH1Bu/DxA/yNR7EmnbxCgxYgm4u7rLTJw8LYJdttHOl+H+++Fv0SQF7PgXAtqkfVg==} + engines: {node: '>= 0.6'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c8@7.14.0: + resolution: {integrity: sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==} + engines: {node: '>=10.12.0'} + hasBin: true + + cacache@10.0.4: + resolution: {integrity: sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==} + + cacache@12.0.4: + resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} + + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + + cache-content-type@1.0.1: + resolution: {integrity: sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==} + engines: {node: '>= 6.0.0'} + + cache-loader@1.2.5: + resolution: {integrity: sha512-enWKEQ4kO3YreDFd7AtVRjtJBmNiqh/X9hVDReu0C4qm8gsGmySkwuWtdc+N5O+vq5FzxL1mIZc30NyXCB7o/Q==} + engines: {node: '>= 4.8 < 5.0.0 || >= 5.10'} + peerDependencies: + webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 + + cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + + cachedir@2.3.0: + resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + call-matcher@1.1.0: + resolution: {integrity: sha512-IoQLeNwwf9KTNbtSA7aEBb1yfDbdnzwjCetjkC8io5oGeOmK2CBNdg0xr+tadRYKO0p7uQyZzvon0kXlZbvGrw==} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + call-signature@0.0.2: + resolution: {integrity: sha512-qvYvkAVcoae0obt8OsZn0VEBHeEpvYIZDy1gGYtZDJG0fHawew+Mi0dBjieFz8F8dzQ2Kr19+nsDm+T5XFVs+Q==} + engines: {node: '>=0.10.0'} + + caller-callsite@2.0.0: + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} + engines: {node: '>=4'} + + caller-path@0.1.0: + resolution: {integrity: sha512-UJiE1otjXPF5/x+T3zTnSFiTOEmJoGTD9HmBoxnCUwho61a2eSNn/VwtwuIBDAo2SEOv1AJ7ARI5gCmohFLu/g==} + engines: {node: '>=0.10.0'} + + caller-path@2.0.0: + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} + engines: {node: '>=4'} + + callsite@1.0.0: + resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==} + + callsites@0.2.0: + resolution: {integrity: sha512-Zv4Dns9IbXXmPkgRRUjAaJQgfN4xX5p6+RQFhWUqscdvvK2xK/ZL8b3IXIJsj+4sD+f24NwnWy2BY8AJ82JB0A==} + engines: {node: '>=0.10.0'} + + callsites@2.0.0: + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} + engines: {node: '>=4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + + camelcase-keys@2.1.0: + resolution: {integrity: sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==} + engines: {node: '>=0.10.0'} + + camelcase-keys@4.2.0: + resolution: {integrity: sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==} + engines: {node: '>=4'} + + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + + camelcase@2.1.1: + resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==} + engines: {node: '>=0.10.0'} + + camelcase@3.0.0: + resolution: {integrity: sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==} + engines: {node: '>=0.10.0'} + + camelcase@4.1.0: + resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==} + engines: {node: '>=4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + capture-stack-trace@1.0.2: + resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} + engines: {node: '>=0.10.0'} + + case-sensitive-paths-webpack-plugin@2.4.0: + resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} + engines: {node: '>=4'} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + ccount@1.1.0: + resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} + + cfork@1.11.0: + resolution: {integrity: sha512-pYyWhuXPq5OoaVPaQZXFf243oQ/+eCXu7ufOZxqi8HFI7TcTxbsi1/16CpT75dSuk78ErMJQlTkpzz9O82G/Ig==} + engines: {node: '>= 0.12.0'} + + chalk@1.1.3: + resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} + engines: {node: '>=0.10.0'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chan@0.6.1: + resolution: {integrity: sha512-/TdBP2UhbBmw7qnqkzo9Mk4rzvwRv4dlNPXFerqWy90T8oBspKagJNZxrDbExKHhx9uXXHjo3f9mHgs9iKO3nQ==} + + change-case@3.1.0: + resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} + + character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + + character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + + character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + + chardet@0.4.2: + resolution: {integrity: sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + + check-types@8.0.3: + resolution: {integrity: sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chokidar@2.1.8: + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@1.6.0: + resolution: {integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + cipher-base@1.0.7: + resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} + engines: {node: '>= 0.10'} + + circular-json-for-egg@1.0.0: + resolution: {integrity: sha512-BzMR1dg0+YqcFoMETHq0gFeQNNKliXI1Oe+C0nx/4npLaohsR7/Oj3UFht65MLwF7zs6x13gOr+f4+JeYni6vw==} + + circular-json@0.3.3: + resolution: {integrity: sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==} + deprecated: CircularJSON is in maintenance only, flatted is its successor. + + circular-json@0.5.9: + resolution: {integrity: sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==} + deprecated: CircularJSON is in maintenance only, flatted is its successor. + + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + + classnames@2.2.6: + resolution: {integrity: sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + + clean-webpack-plugin@0.1.19: + resolution: {integrity: sha512-M1Li5yLHECcN2MahoreuODul5LkjohJGFxLPTjl3j1ttKrF5rgjZET1SJduuqxLAuT1gAPOdkhg03qcaaU1KeA==} + + cli-boxes@1.0.0: + resolution: {integrity: sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==} + engines: {node: '>=0.10.0'} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + cli-color@1.4.0: + resolution: {integrity: sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@2.2.1: + resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@3.2.0: + resolution: {integrity: sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==} + + cliui@5.0.0: + resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + + cls-bluebird@2.1.0: + resolution: {integrity: sha512-XVb0RPmHQyy35Tz9z34gvtUcBKUK8A/1xkGCyeFc9B0C7Zr5SysgFaswRVdwI5NEMcO+3JKlIDGIOgERSn9NdA==} + + cluster-client@3.7.0: + resolution: {integrity: sha512-n0pLGPWlAjaGDJrLKTenjF1qoHbDjuYFlvX4UBoWCt9NjUTZGQhfNafF6Gw4Rj7oJqqdBGrdiIdHSvtOMQX5AA==} + engines: {node: '>=14.0.0'} + + cluster-reload@1.1.0: + resolution: {integrity: sha512-lY3n9ohbJvDsDoaGGx/ER6eqaDKgVVmYjgoSL+XPxI0NHhkr4Ag60RSqVD1B9yZJz/q3FjB94bLfnuGuFuh7aw==} + engines: {node: '>=8.0.0'} + + co-body@6.2.0: + resolution: {integrity: sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==} + engines: {node: '>=8.0.0'} + + co-busboy@1.5.0: + resolution: {integrity: sha512-FCI+YRNcdPt1pH+/5jSHCP0goJpf8vpuKN52gFJy0Az9dnoomdT976O1PcldzOn+MQcYI6xT2lI1lt3Co1C9IA==} + + co-mocha@1.2.2: + resolution: {integrity: sha512-ocdJRn3sxonOqpdjSU2VwTwWzjTSoatzsTqCWiC3eGvJFNs8ZNMlZwfgYolQCdfddMz4muiZl99KIV9gKoNvxg==} + peerDependencies: + mocha: '>=1.18 <6' + + co-request@0.2.1: + resolution: {integrity: sha512-D7bZml3rg7zWPGZ/SVpaXWcsGxFsO28Qb+SvYSQP72hPpf8oEoyTEi6woK5gQx3GzjEaV6tA0uJkNcRqov2u5Q==} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + coa@2.0.2: + resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==} + engines: {node: '>= 4.0'} + + code-point-at@1.1.0: + resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==} + engines: {node: '>=0.10.0'} + + codemirror@5.65.21: + resolution: {integrity: sha512-6teYk0bA0nR3QP0ihGMoxuKzpl5W80FpnHpBJpgy66NK3cZv5b/d/HY8PnRvfSsCG1MTfr92u2WUl+wT0E40mQ==} + + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@1.0.8: + resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@2.13.0: + resolution: {integrity: sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==} + + commander@2.17.1: + resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==} + + commander@2.19.0: + resolution: {integrity: sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + comment-parser@0.5.5: + resolution: {integrity: sha512-oB3TinFT+PV3p8UwDQt71+HkG03+zwPwikDlKU6ZDmql6QX2zFlQ+G0GGSDqyJhdZi4PSlzFBm+YJ+ebOX3Vgw==} + + commitizen@4.3.1: + resolution: {integrity: sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==} + engines: {node: '>= 12'} + hasBin: true + + commitlint@8.3.6: + resolution: {integrity: sha512-JYsamxMfu+8SSQbmsucQ+fJcOMEw5dqvs66Slz0NoRTS5NqDDXb96GtLwbJvUa9HFyOusoZMD24cyoDDn75OPA==} + engines: {node: '>=4'} + hasBin: true + + common-bin@2.9.2: + resolution: {integrity: sha512-fw6YBX8dr4wgMCHqcOR5eIqWZxoZa6+0JAiqjZuOZFCh/pz9hW6EY2EGFc3QpHwp/DlLuw/du/Sr2InhrfAYSQ==} + engines: {node: '>= 6.0.0'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + compare-func@1.3.4: + resolution: {integrity: sha512-sq2sWtrqKPkEXAC8tEJA1+BqAH9GbFkGBtUOqrUX57VSfwp8xyktctk+uLoRy5eccTdxzDcVIztlYDpKs3Jv1Q==} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + component-bind@1.0.0: + resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==} + + component-classes@1.2.6: + resolution: {integrity: sha512-hPFGULxdwugu1QWW3SvVOCUHLzO34+a2J6Wqy0c5ASQkfi9/8nZcBB0ZohaEbXOQlCflMAEMmEWk7u7BVs4koA==} + + component-emitter@1.1.2: + resolution: {integrity: sha512-YhIbp3PJiznERfjlIkK0ue4obZxt2S60+0W8z24ZymOHT8sHloOqWOqZRU2eN5OlY8U08VFsP02letcu26FilA==} + + component-emitter@1.2.1: + resolution: {integrity: sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + component-indexof@0.0.3: + resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==} + + component-inherit@0.0.3: + resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==} + + composition@2.3.0: + resolution: {integrity: sha512-b2wTferUuej6qDfrwPnM6KNd6MHSA1q0/aatvJhUmH2/kUM5wsn4t8sc5vj0NECwsLbAM+H4WitN4d2uL0pIRg==} + + compress-commons@1.2.2: + resolution: {integrity: sha512-SLTU8iWWmcORfUN+4351Z2aZXKJe1tr0jSilPMCZlLPzpdTXnkBW1LevW/MfuANBKJek8Xu9ggqrtVmQrChLtg==} + engines: {node: '>= 0.10.0'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compressing@1.10.5: + resolution: {integrity: sha512-kmVGoqpCTZLy36T8XeYexmyJ8YhZhgjGzkPr2iCGHdfZg7IkelF5DEf5Xyzeo7hwSSEW6ifZuv0IeRAkee5NcA==} + engines: {node: '>= 4.0.0'} + + compression@1.5.2: + resolution: {integrity: sha512-+2fE8M8+Oe0kAlbMPz6UinaaH/HaGf+c5HlWRyYtPga/PHKxStJJKTU4xca8StY0JQ78L2kJaslpgSzCKgHaxQ==} + engines: {node: '>= 0.8.0'} + + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@3.1.5: + resolution: {integrity: sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==} + engines: {node: '>=4'} + + configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} + + connect-history-api-fallback@1.6.0: + resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} + engines: {node: '>=0.8'} + + connect-livereload@0.6.1: + resolution: {integrity: sha512-3R0kMOdL7CjJpU66fzAkCe6HNtd3AavCS4m+uW4KtJjrdGPT0SQEZieAYd+cm+lJoBznNQ4lqipYWkhBMgk00g==} + + connect-timeout@1.6.2: + resolution: {integrity: sha512-qIFt3Ja6gRuJtVoWhPa5FtOO8ERs0MfW/QkmQ0vjrAL78otrkxe8w/qjTAgU/T1W/jH5qeZXJHilmOPKNTiEQw==} + engines: {node: '>= 0.8'} + + connect@2.30.2: + resolution: {integrity: sha512-eY4YHls5bz/g6h9Q8B/aVkS6D7+TRiRlI3ksuruv3yc2rLbTG7HB/7T/CoZsuVH5e2i3S9J+2eARV5o7GIYq8Q==} + engines: {node: '>= 0.8.0'} + deprecated: connect 2.x series is deprecated + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + + constant-case@2.0.0: + resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} + + constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + conventional-changelog-angular@1.6.6: + resolution: {integrity: sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==} + + conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + + conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + + conventional-changelog-cli@2.2.2: + resolution: {integrity: sha512-8grMV5Jo8S0kP3yoMeJxV2P5R6VJOqK72IiSV9t/4H5r/HiRqEBQ83bYGuz4Yzfdj4bjaAEhZN/FFbsFXr5bOA==} + engines: {node: '>=10'} + deprecated: This package is no longer maintained. Please use the conventional-changelog package instead. + hasBin: true + + conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + + conventional-changelog-config-spec@2.1.0: + resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} + + conventional-changelog-conventionalcommits@4.2.1: + resolution: {integrity: sha512-vC02KucnkNNap+foDKFm7BVUSDAXktXrUJqGszUuYnt6T0J2azsbYz/w9TDc3VsrW2v6JOtiQWVcgZnporHr4Q==} + engines: {node: '>=6.9.0'} + + conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + + conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + + conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + + conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + + conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + + conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + + conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + + conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + + conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + + conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + + conventional-commit-types@3.0.0: + resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} + + conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + + conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} + hasBin: true + + conventional-recommended-bump@6.1.0: + resolution: {integrity: sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==} + engines: {node: '>=10'} + hasBin: true + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.3.5: + resolution: {integrity: sha512-YN/8nzPcK5o6Op4MIzAd4H4qUal5+3UaMhVIeaafFYL0pKvBQA/9Yhzo7ZwvBpjdGshsiTAb1+FC37M6RdPDFg==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.1.3: + resolution: {integrity: sha512-mWkFhcL+HVG1KjeCjEBVJJ7s4sAGMLiBDFSDs4bzzvgLZt7rW8BhP6XV/8b1+pNvx/skd3yYxPuaF3Z6LlQzyw==} + + cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookies@0.8.0: + resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} + engines: {node: '>= 0.8'} + + cookies@0.9.1: + resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==} + engines: {node: '>= 0.8'} + + copy-concurrently@1.0.5: + resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} + deprecated: This package is no longer supported. + + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + copy-text-to-clipboard@3.2.2: + resolution: {integrity: sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==} + engines: {node: '>=12'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + copy-to@2.0.1: + resolution: {integrity: sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==} + + copy-webpack-plugin@4.6.0: + resolution: {integrity: sha512-Y+SQCF+0NoWQryez2zXn5J5knmr9z/9qSQt7fbL78u83rxmigOy8X5+BFn8CFSuX+nKT8gpYwJX68ekqtQt6ZA==} + engines: {node: '>= 4'} + + core-js-pure@3.49.0: + resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} + + core-js@1.2.7: + resolution: {integrity: sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@5.2.1: + resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} + engines: {node: '>=4'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cp-file@7.0.0: + resolution: {integrity: sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==} + engines: {node: '>=8'} + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc32-stream@2.0.0: + resolution: {integrity: sha512-UjZSqFCbn+jZUHJIh6Y3vMF7EJLcJWNm4tKDf2peJRwlZKHvkkvOMTvAei6zjU9gO1xONVr3rRFw0gixm2eUng==} + engines: {node: '>= 0.10.0'} + + crc@3.3.0: + resolution: {integrity: sha512-QCx3z7FOZbJrapsnewTkh1Hxh6PHV61SRHbx6Q65Uih3y0kfIj+dDGI3uQ4Q1DLKOILyvpZxvJpoKPrxathpCg==} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + + create-error-class@3.0.2: + resolution: {integrity: sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==} + engines: {node: '>=0.10.0'} + + create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + + create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + + create-react-class@15.7.0: + resolution: {integrity: sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + crequire@1.8.1: + resolution: {integrity: sha512-GbElTY148ZRQbC3E3XlMAitKE9rEyO/2mIkkjwgqzIucRmHiaAMF2Ynpwsuxzp08SdAbeN4pTrEqZs0MWRN6/w==} + engines: {node: '>= 0.6.0'} + + cron-parser@2.18.0: + resolution: {integrity: sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==} + engines: {node: '>=0.8'} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cropperjs@1.6.2: + resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==} + + cross-port-killer@1.4.0: + resolution: {integrity: sha512-ujqfftKsSeorFMVI6JP25xMBixHEaDWVK+NarRZAGnJjR5AhebRQU+g+k/Lj8OHwM6f+wrrs8u5kkCdI7RLtxQ==} + hasBin: true + + cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + + crypto-browserify@3.12.1: + resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} + engines: {node: '>= 0.10'} + + crypto-random-string@1.0.0: + resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} + engines: {node: '>=4'} + + crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + csrf@3.0.6: + resolution: {integrity: sha512-3q1ocniLMgk9nHHEt/I/JsN9IfiGjgp6MHgYNT7+CPmQvi5DF6qzenXnZSH6f9Qaa+4DhmUDJa8SgFZ+OFf9Qg==} + engines: {node: '>= 0.8'} + + csrf@3.1.0: + resolution: {integrity: sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==} + engines: {node: '>= 0.8'} + + css-animation@1.6.1: + resolution: {integrity: sha512-/48+/BaEaHRY6kNQ2OIPzKf9A6g8WjZYjhiNDNuIVbsm5tXCGIAsHDjB4Xu1C4vXJtUWZo26O68OQkDpNBaPog==} + + css-color-names@0.0.4: + resolution: {integrity: sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==} + + css-declaration-sorter@4.0.1: + resolution: {integrity: sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==} + engines: {node: '>4'} + + css-functions-list@3.3.3: + resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} + engines: {node: '>=12'} + + css-loader@3.6.0: + resolution: {integrity: sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==} + engines: {node: '>= 8.9.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + css-select-base-adapter@0.1.1: + resolution: {integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==} + + css-select@2.1.0: + resolution: {integrity: sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==} + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@1.0.0-alpha.37: + resolution: {integrity: sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==} + engines: {node: '>=8.0.0'} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + css-what@3.4.2: + resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} + engines: {node: '>= 6'} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssfilter@0.0.10: + resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + + cssnano-preset-default@4.0.8: + resolution: {integrity: sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==} + engines: {node: '>=6.9.0'} + + cssnano-util-get-arguments@4.0.0: + resolution: {integrity: sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==} + engines: {node: '>=6.9.0'} + + cssnano-util-get-match@4.0.0: + resolution: {integrity: sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==} + engines: {node: '>=6.9.0'} + + cssnano-util-raw-cache@4.0.1: + resolution: {integrity: sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==} + engines: {node: '>=6.9.0'} + + cssnano-util-same-parent@4.0.1: + resolution: {integrity: sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==} + engines: {node: '>=6.9.0'} + + cssnano@4.1.11: + resolution: {integrity: sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==} + engines: {node: '>=6.9.0'} + + csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.4.4: + resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + csurf@1.8.3: + resolution: {integrity: sha512-p2NJ9fGOn5HCaV9jAOBCSjIGMRMrpm9/yDswD0bFi7zQv1ifDufIKI5nem9RmhMsH6jVD6Sx6vs57hnivvkJJw==} + engines: {node: '>= 0.8.0'} + deprecated: This package is archived and no longer maintained. For support, visit https://github.com/expressjs/express/discussions + + currently-unhandled@0.4.1: + resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} + engines: {node: '>=0.10.0'} + + cyclist@1.0.2: + resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} + + cz-conventional-changelog@3.3.0: + resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} + engines: {node: '>= 10'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + dargs@6.1.0: + resolution: {integrity: sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==} + engines: {node: '>=6'} + + dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-urls@1.1.0: + resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==} + + data-urls@2.0.0: + resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} + engines: {node: '>=10'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + date-fns@1.30.1: + resolution: {integrity: sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@2.2.0: + resolution: {integrity: sha512-X0rGvJcskG1c3TgSCPqHJ0XJgwlcvOC7elJ5Y0hYuKBZoVqWpAMfLOeIh2UI/DCQ5ruodIjvsugZtjUYUw2pUw==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@2.3.3: + resolution: {integrity: sha512-dCHp4G+F11zb+RtEu7BE2U8R32AYmM/4bljQfut8LipH3PdwsVBVGh083MXvtKkB7HSQUzSwiXz53c4mzJvYfw==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.6: + resolution: {integrity: sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==} + deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.1.1: + resolution: {integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==} + deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + deep-equal@1.0.1: + resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==} + + deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-user-agent@1.0.0: + resolution: {integrity: sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==} + engines: {node: '>= 0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@1.5.1: + resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} + engines: {node: '>=0.10'} + + depd@1.0.1: + resolution: {integrity: sha512-OEWAMbCkK9IWQ8pfTvHBhCSqHgR+sk5pbiYqq0FqfARG4Cy+cRsCbITx6wh5pcsmfBPiJAcbd98tfdz5fnBbag==} + engines: {node: '>= 0.6'} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + + destroy@1.0.4: + resolution: {integrity: sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + + detect-indent@4.0.0: + resolution: {integrity: sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==} + engines: {node: '>=0.10.0'} + + detect-indent@5.0.0: + resolution: {integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==} + engines: {node: '>=4'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + detect-port@1.6.1: + resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} + engines: {node: '>= 4.0.0'} + hasBin: true + + dicer@0.2.5: + resolution: {integrity: sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==} + engines: {node: '>=0.8.0'} + + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + + diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + + diff@3.5.1: + resolution: {integrity: sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==} + engines: {node: '>=0.3.1'} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + + digest-header@1.1.0: + resolution: {integrity: sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==} + engines: {node: '>= 8.0.0'} + + dingtalk-robot-sender@1.2.0: + resolution: {integrity: sha512-sLSZpjXYz+VpWvuCoOnhPTnIFcVYBdNEGSEQqjYwb09YUmiNUtvhT/VftOxAkfhNykOjmQ4lSY+90lVWErThbw==} + + dir-glob@2.2.2: + resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==} + engines: {node: '>=4'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + directory-named-webpack-plugin@2.3.0: + resolution: {integrity: sha512-syxU/D8OPFvUMZ6yNspHApHyoH7nU/YUwGfUAWxWUlUZOGCV5KArnrFDoS0CxOFF5IMW6RCyW7ADV2RBxxoF+Q==} + peerDependencies: + webpack: '>=2.2.0' + + docsify-cli@4.4.4: + resolution: {integrity: sha512-NAZgg6b0BsDuq/Pe+P19Qb2J1d+ZVbS0eGkeCNxyu4F9/CQSsRqZqAvPJ9/0I+BCHn4sgftA2jluqhQVzKzrSA==} + engines: {node: '>= 10', npm: '>= 6'} + hasBin: true + + docsify-server-renderer@4.13.1: + resolution: {integrity: sha512-XNJeCK3zp+mVO7JZFn0bH4hNBAMMC1MbuCU7CBsjLHYn4NHrjIgCBGmylzEan3/4Qm6kbSzQx8XzUK5T7GQxHw==} + deprecated: docsify-server-renderer 4.x and below is no longer supported while we investigate the future of SSR and SSG for Docsify + + docsify@4.13.1: + resolution: {integrity: sha512-BsDypTBhw0mfslw9kZgAspCMZSM+sUIIDg5K/t1hNLkvbem9h64ZQc71e1IpY+iWsi/KdeqfazDfg52y2Lmm0A==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-align@1.12.4: + resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==} + + dom-closest@0.2.0: + resolution: {integrity: sha512-6neTn1BtJlTSt+XSISXpnOsF1uni1CHsP/tmzZMGWxasYFHsBOqrHPnzmneqEgKhpagnfnfSfbvRRW0xFsBHAA==} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-matches@2.0.0: + resolution: {integrity: sha512-2VI856xEDCLXi19W+4BechR5/oIS6bKCKqcf16GR8Pg7dGLJ/eBOWVbCmQx2ISvYH6wTNx5Ef7JTOw1dRGRx6A==} + + dom-scroll-into-view@1.2.1: + resolution: {integrity: sha512-LwNVg3GJOprWDO+QhLL1Z9MMgWe/KAFLxVWKzjRTxNSPn8/LLDIfmuG71YHznXCqaqTjvHJDYO1MEAgX6XCNbQ==} + + dom-serializer@0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom-urls@1.1.0: + resolution: {integrity: sha512-LNxCeExaNbczqMVfQUyLdd+r+smG7ixIa+doeyiJ7nTmL8aZRrJhHkEYBEYVGvYv7k2DOEBh2eKthoCmWpfICg==} + engines: {node: '>=0.8.0'} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + domain-browser@1.2.0: + resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} + engines: {node: '>=0.4', npm: '>=1.2'} + + domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domexception@1.0.1: + resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==} + deprecated: Use your platform's native DOMException instead + + domexception@2.0.1: + resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} + engines: {node: '>=8'} + deprecated: Use your platform's native DOMException instead + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@2.1.1: + resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} + + dot-prop@3.0.0: + resolution: {integrity: sha512-k4ELWeEU3uCcwub7+dWydqQBRjAjkV9L33HjVRG5Xo2QybI6ja/v+4W73SRi8ubCqJz0l9XsTP1NbewfyqaSlw==} + engines: {node: '>=0.10.0'} + + dot-prop@4.2.1: + resolution: {integrity: sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==} + engines: {node: '>=4'} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotgitignore@2.1.0: + resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} + engines: {node: '>=6'} + + dottie@2.0.7: + resolution: {integrity: sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + draft-js@0.10.5: + resolution: {integrity: sha512-LE6jSCV9nkPhfVX2ggcRLA4FKs6zWq9ceuO/88BpXdNCS7mjRTgs0NsV6piUCJX9YxMsB9An33wnkMmU2sD2Zg==} + peerDependencies: + react: ^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.0-rc || ^16.0.0-rc || ^16.0.0 + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + easy-helper@1.0.2: + resolution: {integrity: sha512-JD1FzLGlWc+D5M1QGEXNhN1JX+t9nTDaLlhVgPMjMSnr+dqFjhFA2sjUN2jkY8RI/RDCL2vmT7BlNgAd7x7aqA==} + engines: {node: '>=6.0.0'} + + easy-puppeteer-html@1.0.2: + resolution: {integrity: sha512-0DU2TE9/KVG0VFFuT3ZzHMJIMy3mowQnhYsvyLKIzy0qkti+zNok6YXCfD9xzEYMSSE7jxQl5CLk8GrbkJN6WQ==} + engines: {node: '>=8.0.0'} + + easywebpack-cli@4.8.1: + resolution: {integrity: sha512-B/DZWSa8vythCHT35Auhu+LWNy8ecO98DwLkOeSF8GizJ397WbNrsvHd4vf0s6HVBhIHUFITpAyI3/0XRBlUvw==} + engines: {node: '>=6.0.0'} + hasBin: true + + easywebpack-react@4.4.5: + resolution: {integrity: sha512-PPSUcXrPErtazn3bP7ArgCiXRKUMdPGWeFtZyyrdpURFrofm8eUTqzi0+3fMPABG75ZCKa0IiV5/CW0dAQfyIA==} + engines: {node: '>=6.0.0'} + + easywebpack@4.12.8: + resolution: {integrity: sha512-5Z07GSjRoShMZxbBiI1bTQtWFyrvl8PxxjktKZlqUQ4D9RE2ZYBZ5EYgAAZHCGJSxvBTZXFOHVCPGAco46BqbQ==} + engines: {node: '>=6.0.0'} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + egg-bin@4.20.0: + resolution: {integrity: sha512-PVvJtt7XrUFeyo2ueGTzGgofN0EVLQSkv8lbRbuWyINQO9FQtjL6Wd4UGyno4xn4nUoIkeXjUiiOhbpessRusw==} + engines: {node: '>= 6.0.0'} + hasBin: true + + egg-cluster@1.27.1: + resolution: {integrity: sha512-zKZV7WS5HwQYXU8TG5lwXe9+iQTw3jZYI4bMzLnRcOuxAhsx0fw1mgeR85Uj9WE+FP0/nexNNjcfFzb6jkzbsw==} + engines: {node: '>= 8.0.0'} + + egg-cookies@2.10.1: + resolution: {integrity: sha512-fjlGF9jIu0GmU0bA+ZGOoz8OhbUECy18Y69wgpt8secmtaFzSbKay3DhKA13evqUDoiag5vQXQaN+N7Pp8vFsA==} + engines: {node: '>= 10.0.0'} + + egg-core@4.31.0: + resolution: {integrity: sha512-qm80BhhXt3VbQQ70EW/t5UzD8FW7CO7w+JGNrv5L14QeglzCPMjMtgwT9BOPqDtGrI+TRhUrxxZjMiseslskMg==} + engines: {node: '>= 8.9.0'} + + egg-cors@2.2.4: + resolution: {integrity: sha512-6yc3pEbpZVLiCRdvR/Y+fT1sIO8/IldA31xQJeQDtgPrPWS99LXGyxWkszSovUS77yOpciSlcnpNoY9HAUP9hA==} + engines: {node: '>=8.0.0'} + + egg-development@2.7.0: + resolution: {integrity: sha512-vPkC+Wngm/a48bj0rXpvTOmSjslBnygCsjBG7qahJ1PuCs0f10KrOGOGY9JzOW6KpfjKSPb+Y21mpzkxBJ9q2Q==} + engines: {node: '>=8.0.0'} + + egg-errors@2.3.2: + resolution: {integrity: sha512-E+Sx7IBVrfRyHSjFXaq4sCZ3Uk3ka9PYySaQ8VbRZmLEt9ENBCD99yVzLIeWUH2QfzvkrjY9El1eHmLeRx7cfw==} + engines: {node: '>=8.9.0'} + + egg-i18n@2.1.1: + resolution: {integrity: sha512-rpKP2nrUzeTOkjQObvlrLbb/BZAMtP7zeoGggwJLN9+zKbFKVkeF6Q0BrpuucDSekD+2oHQz7fC3w/5eQY1g8w==} + engines: {node: '>= 8.0.0'} + + egg-jsonp@2.0.0: + resolution: {integrity: sha512-dno7BXAvSFO0WTb3GJD0aci9MFlaQhdgvEznwARam75GGJdLpg1K0XQq88/dEvYWHxEHa+iGIOXcNx5NOyyUHw==} + engines: {node: '>=8.0.0'} + + egg-logger@1.8.0: + resolution: {integrity: sha512-nnf+xc/KfDcWsCFk17SZU9VcUX0fViHns7Vm4yt1AijJRwnJeELeF+nQjmVZ2BH+BOJ9FaQrThJBzEHDCWHzFg==} + engines: {node: '>= 4.0.0'} + + egg-logger@2.9.1: + resolution: {integrity: sha512-TPYdNthc7yGV+08A2U4g1T1wgRAjfTnsYC53JgfdKiYukaH3na1KPXEu+TEWni7IooqkFGkQ0t0WY+ylWFnvbw==} + engines: {node: '>=8.5.0'} + + egg-logger@3.6.1: + resolution: {integrity: sha512-lGiEumARJAT7NiwMm577NFiOEBMX9FlZzfuBsLLS6eu14R9udcTI1IzXLGJ63SrCmFIXB+zsWLQv5U11e8fq0Q==} + engines: {node: '>=14.17.0'} + + egg-logrotator@3.2.0: + resolution: {integrity: sha512-2KYN4wwBd15FYbNtLxL7IoIkFL7JuHHA3eXWZd5zVKfWgeoy4J+oTnCLDixW1UhVIdehL0q1oQxEldWm5af1Eg==} + engines: {node: '>=8.0.0'} + + egg-multipart@2.13.1: + resolution: {integrity: sha512-WZKJN3/6O0PKnSHvz22TSk7t8vWvEcexdhpb/zoKsKSoDAZgd0ffk3dy+dZR4+woePEth/gibHY8QZgp1CUFlA==} + engines: {node: '>= 8.0.0'} + + egg-onerror@2.4.0: + resolution: {integrity: sha512-btmUsP1m6H9awd1IaAlvlzqN+uIbUKzaxJ8ArZI71r5FQ9LShZGv2pOT5fhL9mSnGkrvHUsjb9I5KoX2AY63bA==} + engines: {node: '>=8.0.0'} + + egg-path-matching@1.2.0: + resolution: {integrity: sha512-u88hY0tH8GfWu4iocWFxux/abXvi7bh4lr/GIRlUHCTJFk8RQNCCfk5H3Vm67pqTBljtBOCl0lLykOsxj8trXQ==} + + egg-schedule@3.7.0: + resolution: {integrity: sha512-kXnqOOk+IpXpov9gpy4Bv1977iiwlk4kRNHPcp2O/Lp934tw9Lx1A2CDxv9lDrazhY7BqQFY7Nq+j8x74yog0w==} + engines: {node: '>=8.0.0'} + + egg-scripts@2.17.0: + resolution: {integrity: sha512-2OHW0HuKKhOsl3yczYW6vcweyDrtFVOBccJ5xfuZRJOM3wTLRKAIfvXO/oNV1Mo0LSVuphhHiWmrUp481hcRXA==} + engines: {node: '>=6.0.0'} + hasBin: true + + egg-security@2.11.0: + resolution: {integrity: sha512-htXi+R5Ik8/oKSy55LzmpFDzrzxSHLpfjbDtgSMGKYGl4uNuvb3kN2h00sl1rOz3pG/LzJvZhqxW1oIBQ7leXg==} + engines: {node: '>=8.0.0'} + + egg-sequelize@4.3.1: + resolution: {integrity: sha512-C+hunArmved0OVMGo5sKz6TDhdYCdXCsqQiiyOlezfZbQUYEAaKwPaNLjb9bM0fA6/aNQurT19F9GRlAcInx4A==} + engines: {node: '>=6.0.0'} + + egg-session@3.3.0: + resolution: {integrity: sha512-RCPWHLWi0Ak+xI/zXN71Wpva/wsqevmKDvkOGk2uu5UdwRHw5lKuTtYm161NMDGZi6lvcnzR2lN7xRcDuhGiIw==} + engines: {node: '>=8.0.0'} + + egg-socket.io@4.1.6: + resolution: {integrity: sha512-eIA1RydCrGLWTjvHx0sbtRHTopXPvd1aOcClrAeawdhOce12T0sh7QJGP9C1cfbH6Qaim89m0mFgR04JNn9ULg==} + engines: {node: '>=8.0.0'} + + egg-ssh@1.0.5: + resolution: {integrity: sha512-2S2qtMypyUbRb8Gc+rxZnhtlRcf4vtrtFysnW3rH1KcMYFSwcYXLI8wKJOZO9nKiwTwCNIGUTAHaI3u3PEEU2g==} + engines: {node: '>=8.0.0'} + + egg-static@2.3.1: + resolution: {integrity: sha512-OODn2ccm4znFM/EdhiPUmIpgCkYvAdtjuVAy3N/Ub4cSRgqIPcjXajmDsbhEo43mCieukPhsOxgWD3vKZurzHQ==} + engines: {node: '>=14.0.0'} + + egg-ts-helper@1.35.2: + resolution: {integrity: sha512-KSNjFt/7QjieFf56rhDnoL7kAJSp78rQwwRKNzg+P3caFGR1LHxkTU+GWjrJk2iZfYQrP/pUPQ7Q4BeQhxG4zA==} + engines: {node: '>= 14.0.0'} + hasBin: true + + egg-utils@2.5.0: + resolution: {integrity: sha512-zQDXcqD0v+6IDBxcxzpTWoDTMg0G3iISSSeOHN7dZzyJWXXmw4ijBPAwKQPVvqBoXB6jAXj5f8B8/LVM9AN//A==} + + egg-validate@1.1.2: + resolution: {integrity: sha512-j42EFW8wzfAe+wQnEQyRBdVxeW+UMLrmW9n3/heyZkwfRYxhiXeJldD4QpGtS+xq1vnhyNu+uMXOymcxUirN/A==} + engines: {node: '>=6.0.0'} + + egg-view-react-ssr@2.5.4: + resolution: {integrity: sha512-/FDVLdgcWlGweZmWqXuxC5l0r0xn5BtSteTGjlZMw38WwR0RFLOqk6ucumzj34CafNueW7VnW2sFfkrsl2dhqg==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: '*' + react-dom: '*' + + egg-view@2.1.4: + resolution: {integrity: sha512-8zJ/S7YU5SK3EbAYESlUCixLpDZPnyKimoowavrgW2vONnh8AJVuDGqgKCYmuymvWq3uaHoNx7mYbdMQwMTZ6A==} + engines: {node: '>=8.0.0'} + + egg-watcher@3.1.1: + resolution: {integrity: sha512-fLo8f2GD9kSrAKeDoXaCckl9MaMMwTEkqU9gVDYWYGPYLsmX79ugA+Wo/2RGQkytsxSCk3bn8YqhPdMzsozgWA==} + engines: {node: '>= 8.0.0'} + + egg-webpack-react@2.0.3: + resolution: {integrity: sha512-CuJyi00DTgzjsVTwUhBB3KhqziNfuyUROELX+oP9jMkYUPjTrwCvkRci85SLFYvTM/+z2rKbDtG1GsA9NC/PCA==} + engines: {node: '>=6.0.0'} + + egg-webpack@4.5.5: + resolution: {integrity: sha512-w1l5i0dHLonFBF63vWyvLQibhkOK0x7/AZ8Fb+HaNIu/4LVjllc1qh6ZXHHJCPgA+616v7hPvZ3vbtIDsrVZNQ==} + engines: {node: '>=6.0.0'} + + egg@2.37.0: + resolution: {integrity: sha512-PQU6Z9cji4Q2eV+1pyO4BLdnszDUyPZkj3v7RfMIACzYFcEKHj3agdTrjtpI74zNZqhO6LWOqvEKIgC+37v1lA==} + engines: {node: '>= 8.5.0'} + + ejs@2.7.4: + resolution: {integrity: sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==} + engines: {node: '>=0.10.0'} + + electron-to-chromium@1.5.360: + resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} + + elliptic@6.6.1: + resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + + emoji-regex@7.0.3: + resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emojis-list@2.1.0: + resolution: {integrity: sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==} + engines: {node: '>= 0.10'} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + empower-assert@1.1.0: + resolution: {integrity: sha512-Ylck0Q6p8y/LpNzYeBccaxAPm2ZyuqBgErgZpO9KT0HuQWF0sJckBKCLmgS1/DEXEiyBi9XtYh3clZm5cAdARw==} + + empower-core@1.2.0: + resolution: {integrity: sha512-g6+K6Geyc1o6FdXs9HwrXleCFan7d66G5xSCfSF7x1mJDCes6t0om9lFQG3zOrzh3Bkb/45N0cZ5Gqsf7YrzGQ==} + + empower@1.3.1: + resolution: {integrity: sha512-uB6/ViBaawOO/uujFADTK3SqdYlxYNn+N4usK9MRKZ4Hbn/1QSy8k2PezxCA2/+JGbF8vd/eOfghZ90oOSDZCA==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@1.8.1: + resolution: {integrity: sha512-9rinVRuulTzDaHJfYGHfk+U/SaZmCHlCQ/UDk3dZIQcLFflscj8pHwNbcHK1wtDwIebIerin3yD6eoZmLlEhKg==} + + engine.io-client@3.5.6: + resolution: {integrity: sha512-2fDMKiXSU7bGRDCWEw9cHEdRNfoU8cpP6lt+nwJhv72tSJpO7YBsqMqYZ63eVvwX3l9prPl2k/mxhfVhY+SDWg==} + + engine.io-parser@1.3.1: + resolution: {integrity: sha512-apg+90JYifyXR0Ju+dweByTyC8AKj1pY18643GVAy0lIHh2Q38EIGks8p93acvpjLOgsqRNV+fyALTLV7Wqm1g==} + + engine.io-parser@2.2.1: + resolution: {integrity: sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@3.6.2: + resolution: {integrity: sha512-C4JjGQZLY3kWlIDx0BQNKizbrfpb7NahxDztGdN5jrPK2ghmXiNDN+E/t0JzDeNRZxPVaszxEng42Pmj27X/0w==} + engines: {node: '>=8.0.0'} + + engine.io@6.6.8: + resolution: {integrity: sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@3.4.1: + resolution: {integrity: sha512-ZaAux1rigq1e2nQrztHn4h2ugvpzZxs64qneNah+8Mh/K0CRqJFJc+UoXnUsq+1yX+DmQFPPdVqboKAJ89e0Iw==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + + enhanced-resolve@4.5.0: + resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==} + engines: {node: '>=6.9.0'} + + enquire.js@2.1.6: + resolution: {integrity: sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-inject@1.0.0: + resolution: {integrity: sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg==} + + errorhandler@1.4.3: + resolution: {integrity: sha512-pp1hk9sZBq4Bj/e/Cl84fJ3cYiQDFZk3prp7jrurUbPGOlY7zA2OubjhhEAWuUb8VNTFIkGwoby7Uq6YpicfvQ==} + engines: {node: '>= 0.8'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.2: + resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-map@0.1.5: + resolution: {integrity: sha512-mz3UqCh0uPCIqsw1SSAkB/p0rOzF/M0V++vyN7JqlPtSW/VsYgQBvVvqMLmfBuyMzTpLnNqi6JmcSizs4jy19A==} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + es6-set@0.1.6: + resolution: {integrity: sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw==} + engines: {node: '>=0.12'} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escallmatch@1.5.0: + resolution: {integrity: sha512-iMF4I4I2E16DPusKDgTtQeIBNX0oOS53Ih6sr/2fh+1SDRsXvG8Y3ZOXGWlDkNNo066XBIkfaDRLfZpqcD+vGA==} + + escape-goat@2.1.1: + resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} + engines: {node: '>=8'} + + escape-html@1.0.2: + resolution: {integrity: sha512-J5ahyCRC4liskWVAfkmosNWfG0eHQxI0W+Ko7k3cZaYVMfgt05dwZ68vw6S/TZM1BPvuTv3kq6CRCb7WWtBUVA==} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + escope@3.6.0: + resolution: {integrity: sha512-75IUQsusDdalQEW/G/2esa87J7raqdJF+Ca0/Xm5C3Q58Nr4yVYjZGp/P1+2xiEVgXRrA39dpRb8LcshajbqDQ==} + engines: {node: '>=0.4.0'} + + eslint-config-egg@5.1.1: + resolution: {integrity: sha512-RihSm4TOm0urZkwjckASKqQcnSJUTCEc/rUqeeFqVxxUsqMV9q0nSOE7aAccwgzIoBsdIV8moLof9DTrHYpzYA==} + engines: {node: '>= 4.0.0'} + + eslint-config-egg@7.5.1: + resolution: {integrity: sha512-HNu+M0Od4HH7SMqo17oFe2n2mwMcKaNA52SQuCXIb3i0KazBxvaaa9DmNvoymQXlbxsZYtSN1J1eKmfZJiISkg==} + engines: {node: '>=8.0.0'} + + eslint-config-prettier@8.5.0: + resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-config-standard@17.0.0: + resolution: {integrity: sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==} + peerDependencies: + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: ^15.0.0 + eslint-plugin-promise: ^6.0.0 + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-loader@2.2.1: + resolution: {integrity: sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg==} + deprecated: This loader has been deprecated. Please use eslint-webpack-plugin + peerDependencies: + eslint: '>=1.6.0 <7.0.0' + webpack: '>=2.0.0 <5.0.0' + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-dt-react@0.0.6: + resolution: {integrity: sha512-yLSfUl7CF0XED34Zh2hwtGZyXxlXt6Nf8hcte7S32UUnDMcFudF1pHWUa4C34LQap/aSywb9qsUHWIlpoab1bQ==} + + eslint-plugin-eggache@1.0.0: + resolution: {integrity: sha512-LPTrTvITFDZggiXAIdMPL4bJo0wvXUgJqC3f6UIskJxzHZze2aBTvjWQJ7TgEbkfpk++KWhcOl+lels+qAPKDg==} + engines: {node: '>=6.0.0'} + + eslint-plugin-es@4.1.0: + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + + eslint-plugin-import@2.26.0: + resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jest@26.5.3: + resolution: {integrity: sha512-sICclUqJQnR1bFRZGLN2jnSVsYOsmPYYnroGCIMVSvTS3y8XR3yjzy1EcTQmk6typ5pRgyIWzbjqxK6cZHEZuQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + + eslint-plugin-jsdoc@4.8.4: + resolution: {integrity: sha512-VDP+BI2hWpKNNdsJDSPofSQ9q7jGLgWbDMI0LzOeEcfsTjSS7jQtHDUuVLQ5E+OV2MPyQPk/3lnVcHfStXk5yA==} + engines: {node: '>=4'} + peerDependencies: + eslint: '>=4.14.0' + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-jsx-a11y@6.6.0: + resolution: {integrity: sha512-kTeLuIzpNhXL2CwLlc8AHI0aFRwWHcg483yepO9VQiHzM9bZwJdzTkzBszbuPrbgGmq2rlX/FaT2fJQsjUSHsw==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + eslint-plugin-n@15.2.3: + resolution: {integrity: sha512-H+KC7U5R+3IWTeRnACm/4wlqLvS1Q7M6t7BGhn89qXDkZan8HTAEv3ouIONA0ifDwc2YzPFmyPzHuNLddNK4jw==} + engines: {node: '>=12.22.0'} + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@4.2.1: + resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + eslint: '>=7.28.0' + eslint-config-prettier: '*' + prettier: '>=2.0.0' + peerDependenciesMeta: + eslint-config-prettier: + optional: true + + eslint-plugin-promise@6.0.0: + resolution: {integrity: sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + eslint-plugin-react-hooks@4.6.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react@7.30.1: + resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-simple-import-sort@10.0.0: + resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-plugin-sort-requires@2.1.0: + resolution: {integrity: sha512-kU24IuceSOEMfASJF0Blmy3mcsLWfUaUNSaEAz3v80TXBi9HbvTBLA0iISlmLZvvxRlegNTRxFYVgsvcAkz6PA==} + + eslint-scope@3.7.1: + resolution: {integrity: sha512-ivpbtpUgg9SJS4TLjK7KdcDhqc/E3CGItsvQbBNLkNGUeMhd5qnJcryba/brESS+dg3vrLqPuc/UcS7jRJdN5A==} + engines: {node: '>=4.0.0'} + + eslint-scope@3.7.3: + resolution: {integrity: sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==} + engines: {node: '>=4.0.0'} + + eslint-scope@4.0.3: + resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==} + engines: {node: '>=4.0.0'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-utils@3.0.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@4.19.1: + resolution: {integrity: sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==} + engines: {node: '>=4'} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + eslint@8.22.0: + resolution: {integrity: sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espower-loader@1.2.2: + resolution: {integrity: sha512-b2S362kHB3hDc8DIW7j3K6fIO+fMhwN+/1HimzmTRRe5Tl9Ox83WvVNjO4QL+HdplbSCw5VvHJpMIxgzEu+Rcw==} + engines: {node: '>= 0.8.0'} + + espower-location-detector@1.0.0: + resolution: {integrity: sha512-Y/3H6ytYwqC3YcOc0gOU22Lp3eI5GAFGOymTdzFyfaiglKgtsw2dePOgXY3yrV+QcLPMPiVYwBU9RKaDoh2bbQ==} + + espower-source@2.3.0: + resolution: {integrity: sha512-Wc4kC4zUAEV7Qt31JRPoBUc5jjowHRylml2L2VaDQ1XEbnqQofGWx+gPR03TZAPokAMl5dqyL36h3ITyMXy3iA==} + engines: {node: '>=0.8.0', npm: '>=1.2.10'} + + espower@2.1.2: + resolution: {integrity: sha512-2qa3aEFtcgPB782jTKDPu82hOdw8+zJsWdOn12Tey8XlexHTqsYUIdLC2B7cUECENXly0vZblH1CEZcqttPNjw==} + + espree@3.5.4: + resolution: {integrity: sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==} + engines: {node: '>=0.10.0'} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@2.7.3: + resolution: {integrity: sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==} + engines: {node: '>=0.10.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + espurify@1.8.1: + resolution: {integrity: sha512-ZDko6eY/o+D/gHCWyHTU85mKDgYcS4FJj7S+YD6WIInm7GQ6AnOjmcL4+buFV/JOztVLELi/7MmuGU5NHta0Mg==} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.7.0: + resolution: {integrity: sha512-Mbv5pNpLNPrm1b4rzZlZlfTRpdDr31oiD43N362sIyvSWVNu5Du33EcJGzvEV4YdYLuENB1HzND907cQkFmXNw==} + engines: {node: '>= 0.6'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventlistener@0.0.1: + resolution: {integrity: sha512-hXZ5N9hmp3n7ovmVgG+2vIO6KcjSU10/d0A1Ixcf0i29dxCwAGTNGrSJCfLmlvmgQD8FYzyp//S8+Hpq4Nd7uA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + + execa@0.7.0: + resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} + engines: {node: '>=4'} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@3.4.0: + resolution: {integrity: sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==} + engines: {node: ^8.12.0 || >=9.7.0} + + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express-session@1.11.3: + resolution: {integrity: sha512-QdSbGRRg+JMvlYpancRDFXDmIMqjEdpowriwQc4Kz3mvPwTnOPD/h5FSS21+4z4Isosta+ULmEwL6F3/lylWWg==} + engines: {node: '>= 0.8.0'} + + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + extend2@1.0.1: + resolution: {integrity: sha512-ISoKeVhtewd5YHzMo+r9KC3Zx0fdpNBqoRzot+6BeEQ3bWQYQQOt0jkkY5gLveI2e7j+vdCJKeszHJIbg2Uceg==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + external-editor@2.2.0: + resolution: {integrity: sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==} + engines: {node: '>=0.12'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@1.1.0: + resolution: {integrity: sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@2.2.7: + resolution: {integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==} + engines: {node: '>=4.0.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fault@1.0.4: + resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==} + + fbjs@0.8.18: + resolution: {integrity: sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA==} + + fd-slicer2@1.2.0: + resolution: {integrity: sha512-3lBUNUckhMZduCc4g+Pw4Ve16LD9vpX9b8qUkkKq2mgDRLYWzblszZH2luADnJqjJe+cypngjCuKRm/IW12rRw==} + + figgy-pudding@3.5.2: + resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} + deprecated: This module is no longer supported. + + figlet@1.11.0: + resolution: {integrity: sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==} + engines: {node: '>= 17.0.0'} + hasBin: true + + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@2.0.0: + resolution: {integrity: sha512-uXP/zGzxxFvFfcZGgBIwotm+Tdc55ddPAzF7iHshP4YGaXMww7rSF9peD9D1sui5ebONg5UobsZv+FfgEpGv/w==} + engines: {node: '>=0.10.0'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-loader@1.1.11: + resolution: {integrity: sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==} + engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'} + peerDependencies: + webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 + + file-loader@3.0.1: + resolution: {integrity: sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + + file-saver@1.3.8: + resolution: {integrity: sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filesize@3.6.1: + resolution: {integrity: sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==} + engines: {node: '>= 0.4.0'} + + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@0.4.0: + resolution: {integrity: sha512-jJU2WE88OqUvwAIf/1K2G2fTdKKZ8LvSwYQyFFekDcmBnBmht38enbcmErnA7iNZktcEo/o2JAHYbe1QDOAgaA==} + engines: {node: '>= 0.8'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-cache-dir@0.1.1: + resolution: {integrity: sha512-Z9XSBoNE7xQiV6MSgPuCfyMokH2K7JdpRkOYE1+mu3d4BFJtx3GW+f6Bo4q8IX6rlf5MYbLBKW0pjl2cWdkm2A==} + engines: {node: '>=0.10.0'} + + find-cache-dir@1.0.0: + resolution: {integrity: sha512-46TFiBOzX7xq/PcSWfFwkyjpemdRnMe31UQF+os0y+1W3k95f6R4SEt02Hj4p3X0Mir9gfrkmOtshFidS0VPUg==} + engines: {node: '>=4'} + + find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + + find-node-modules@2.1.3: + resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@1.1.2: + resolution: {integrity: sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==} + engines: {node: '>=0.10.0'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + + flat-cache@1.3.4: + resolution: {integrity: sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==} + engines: {node: '>=0.10.0'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat@4.1.1: + resolution: {integrity: sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==} + hasBin: true + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + flush-write-stream@1.1.1: + resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} + + flushwritable@1.0.0: + resolution: {integrity: sha512-3VELfuWCLVzt5d2Gblk8qcqFro6nuwvxwMzHaENVDHI7rxcBRtMCwTk/E9FXcgh+82DSpavPNDueA9+RxXJoFg==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + follow-redirects@1.5.10: + resolution: {integrity: sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==} + engines: {node: '>=4.0'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + + form-data@3.0.4: + resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + formstream@1.5.2: + resolution: {integrity: sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + + fresh@0.3.0: + resolution: {integrity: sha512-akx5WBKAwMSg36qoHTuMMVncHWctlaDGslJASDYAhoLrzDUDCjZlOngNa/iC6lPm9aA0qk8pN5KnpmbJHSIIQQ==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@0.30.0: + resolution: {integrity: sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==} + + fs-extra@5.0.0: + resolution: {integrity: sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-readdir-recursive@1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + + fs-write-stream-atomic@1.0.10: + resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} + deprecated: This package is no longer supported. + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: Upgrade to fsevents v2 to mitigate potential security issues + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gals@1.0.2: + resolution: {integrity: sha512-h5c1Q6Q2cnRkO2v8ZxbuFCNRpM96CjGxGuoNcThoNF3dAEEYagF166EqJmaa9r2/I+ryij8TO3yMmqrMvQ1YXw==} + engines: {node: '>= 16.0.0'} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + generic-pool@3.5.0: + resolution: {integrity: sha512-dEkxmX+egB2o4NR80c/q+xzLLzLX+k68/K8xv81XprD+Sk7ZtP14VugeCz+fUwv5FzpWq40pPtAkzPRqT8ka9w==} + engines: {node: '>= 4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@1.0.3: + resolution: {integrity: sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-ready@1.0.0: + resolution: {integrity: sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==} + + get-ready@2.0.1: + resolution: {integrity: sha512-q2zoHPGbHognNvofksty0SOrviWowfUVxYcv3j3+Mf1BRRMHmq6q/1pciravEf8jkSL5iovloMWnonEc2QOpqA==} + engines: {node: '>= 4.0.0'} + + get-stdin@4.0.1: + resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} + engines: {node: '>=0.10.0'} + + get-stdin@7.0.0: + resolution: {integrity: sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==} + engines: {node: '>=8'} + + get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. + hasBin: true + + git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + + git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + deprecated: This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead. + hasBin: true + + gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + + glob-parent@3.1.0: + resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.1.0: + resolution: {integrity: sha512-zNKwUvfFs4IbHMLzBDl4v5YbFNs64e4yGkptl4DncCYwmhMQORQflvs7XsEv50+M5bJqbgjBqnV+zZ8vF490yQ==} + + glob-to-regexp@0.3.0: + resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.1.3: + resolution: {integrity: sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-directory@5.0.0: + resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} + engines: {node: '>=20'} + + global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + + global-dirs@2.1.0: + resolution: {integrity: sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==} + engines: {node: '>=8'} + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@9.18.0: + resolution: {integrity: sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==} + engines: {node: '>=0.10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@7.1.1: + resolution: {integrity: sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==} + engines: {node: '>=4'} + + globby@9.2.0: + resolution: {integrity: sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==} + engines: {node: '>=6'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@6.7.1: + resolution: {integrity: sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==} + engines: {node: '>=4'} + + got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graceful-process@1.3.0: + resolution: {integrity: sha512-tlldpcdxQgSgVD/A+OawlyjGU9Z+wbBTI6QroGo4u6znQNLgglp+V2KnixRHUXRttnrCpbQSSCJ6wvsMkzR9Aw==} + + graceful@1.1.0: + resolution: {integrity: sha512-sImEQVLLBo8hSeX4Vp6HVdWSdqCkHoVlZwBuBL65tNwcPTvnMaW40iWs1vdCbyP0znkCEuvv6rZ37QPloPi9Fw==} + engines: {node: '>= 0.10.0'} + + grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + + growl@1.10.5: + resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} + engines: {node: '>=4.x'} + + gud@1.0.0: + resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} + + gzip-size@5.1.1: + resolution: {integrity: sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==} + engines: {node: '>=6'} + + hammerjs@2.0.8: + resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} + engines: {node: '>=0.8.0'} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-ansi@2.0.0: + resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} + engines: {node: '>=0.10.0'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-binary2@1.0.3: + resolution: {integrity: sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==} + + has-binary@0.1.6: + resolution: {integrity: sha512-aBByfHrIiIt6PQ+jFXsLIFVNpHVyXDcCZ77VZ4kvxv6TvTwipSTDNvKnPN5xOi/cQTcxhLa4lBV2b49pZGQgXw==} + + has-binary@0.1.7: + resolution: {integrity: sha512-k1Umb4/jrBWZbtL+QKSji8qWeoZ7ZTkXdnDXt1wxwBKAFM0//u96wDj43mBIqCIas8rDQMYyrBEvcS8hdGd4Sg==} + + has-cors@1.1.0: + resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + + has-yarn@2.1.0: + resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} + engines: {node: '>=8'} + + has@1.0.4: + resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==} + engines: {node: '>= 0.4.0'} + + hash-base@3.0.5: + resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==} + engines: {node: '>= 0.10'} + + hash-base@3.1.2: + resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} + engines: {node: '>= 0.8'} + + hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-parse-selector@2.2.5: + resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} + + hastscript@6.0.0: + resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + header-case@1.0.1: + resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} + + hex-color-regex@1.1.0: + resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + highlightjs-vue@1.0.0: + resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + + history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + + hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + + hoist-non-react-statics@2.5.5: + resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + home-or-tmp@2.0.0: + resolution: {integrity: sha512-ycURW7oUxE2sNiPVw1HVEFsW+ecOpJ5zaj7eC0RlwhibhRBod20muUN8qu/gzx956YrLolVvs1MTXwKgC2rVEg==} + engines: {node: '>=0.10.0'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hono@4.12.21: + resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==} + engines: {node: '>=16.9.0'} + + hoopy@0.1.4: + resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} + engines: {node: '>= 6.0.0'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hsl-regex@1.0.0: + resolution: {integrity: sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==} + + hsla-regex@1.0.0: + resolution: {integrity: sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==} + + html-encoding-sniffer@1.0.2: + resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==} + + html-encoding-sniffer@2.0.1: + resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} + engines: {node: '>=10'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier@3.5.21: + resolution: {integrity: sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==} + engines: {node: '>=4'} + hasBin: true + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-webpack-plugin@3.2.0: + resolution: {integrity: sha512-Br4ifmjQojUP4EmHnRBoUIYcZ9J7M4bTMcm7u6xoIAIuq2Nte4TzXX0533owvkQKQD1WeMTTTyD4Ni4QKxS0Bg==} + engines: {node: '>=6.9'} + deprecated: 3.x is no longer supported + peerDependencies: + webpack: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + + html2canvas@0.5.0-beta4: + resolution: {integrity: sha512-Tlyu46Ua0gnXz8tLAzITmqlp5PR8HTfhE12fWsIU3VO6Mn4PmVFQpvp3pQ35nFARinw5X8u3t2XjqYyS+hq/kA==} + engines: {node: '>=4.0.0'} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + http-assert@1.5.0: + resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==} + engines: {node: '>= 0.8'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-errors@1.3.1: + resolution: {integrity: sha512-gMygNskMurDCWfoCdyh1gOeDfSbkAHXqz94QoPj5IHIUjC/BG8/xv7FSEUr7waR5RcAya4j58bft9Wu/wHNeXA==} + engines: {node: '>= 0.6'} + + http-errors@1.8.1: + resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==} + engines: {node: '>= 0.6'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-middleware@0.20.0: + resolution: {integrity: sha512-dNJAk71nEJhPiAczQH9hGvE/MT9kEs+zn2Dh+Hi94PGZe1GluQirC7mw5rdREUtWx6qGS1Gu0bZd4qEAg+REgw==} + engines: {node: '>=8.0.0'} + + http-proxy-middleware@2.0.6: + resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + + https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + humanize-bytes@1.0.1: + resolution: {integrity: sha512-OZMIHt5YhQM4S3R82KGoKnWv4vaNnWuEhPAK+u5v7rD2tRM3DU2NghYEHipAPYwyTR6+fMVZQ9ETpZIZeZUapQ==} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + husky@3.1.0: + resolution: {integrity: sha512-FJkPoHHB+6s4a+jwPqBudBDvYZsoQW5/HBuMSehC8qDiCe50kpcxeqFoDSlow+9I6wg47YxBoT3WxaURlrDIIQ==} + engines: {node: '>=8.6.0'} + hasBin: true + + iconv-lite@0.2.11: + resolution: {integrity: sha512-KhmFWgaQZY83Cbhi+ADInoUQ8Etn6BG5fikM9syeOjQltvR45h7cRKJ/9uvQEuD61I3Uju77yYce0/LhKVClQw==} + engines: {node: '>=0.4.0'} + + iconv-lite@0.4.11: + resolution: {integrity: sha512-8UmnaYeP5puk18SkBrYULVTiq7REcimhx+ykJVJBiaz89DQmVQAfS29ZhHah86la90/t0xy4vRk86/2cCwNodA==} + engines: {node: '>=0.8.0'} + + iconv-lite@0.4.13: + resolution: {integrity: sha512-QwVuTNQv7tXC5mMWFX5N5wGjmybjNBBD8P3BReTkPmipoxTUFgWM2gXNvldHQr6T14DH0Dh6qBVg98iJt7u4mQ==} + engines: {node: '>=0.8.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.5.2: + resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + icss-utils@4.1.1: + resolution: {integrity: sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==} + engines: {node: '>= 6'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + iferr@0.1.5: + resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==} + + ignore@3.3.10: + resolution: {integrity: sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==} + + ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + immutable@3.7.6: + resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} + engines: {node: '>=0.8.0'} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + import-cwd@2.1.0: + resolution: {integrity: sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==} + engines: {node: '>=4'} + + import-fresh@2.0.0: + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} + engines: {node: '>=4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-from@2.1.0: + resolution: {integrity: sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==} + engines: {node: '>=4'} + + import-lazy@2.1.0: + resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} + engines: {node: '>=4'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@2.1.0: + resolution: {integrity: sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==} + engines: {node: '>=0.10.0'} + + indent-string@3.2.0: + resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==} + engines: {node: '>=4'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + indexes-of@1.0.1: + resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} + + indexof@0.0.1: + resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflation@2.1.0: + resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==} + engines: {node: '>= 0.8.0'} + + inflection@1.12.0: + resolution: {integrity: sha512-lRy4DxuIFWXlJU7ed8UiTJOSTqStqYdEb4CEbtXfNbkdj3nH1L+reUWiE10VWcJS2yR7tge8Z74pJjtBjNwj0w==} + engines: {'0': node >= 0.4.0} + + inflection@1.13.4: + resolution: {integrity: sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==} + engines: {'0': node >= 0.4.0} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.7: + resolution: {integrity: sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inquirer@3.3.0: + resolution: {integrity: sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==} + + inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + + inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + + insert-css@2.0.0: + resolution: {integrity: sha512-xGq5ISgcUP5cvGkS2MMFLtPDBtrtQPSFfC6gA6U8wHKqfjTIMZLZNxOItQnoSjdOzlXOLU/yD32RKC4SvjNbtA==} + + inspector-proxy@1.2.3: + resolution: {integrity: sha512-5YkxR72v8oVpSE3nL3dhpO2WNS9Gg1Pp1mWulC1BCNNG8g+SiDzuJTkUgM3xckUmz6jS0YOSpgcYM6fJxbz5fA==} + engines: {node: '>=6.0.0'} + hasBin: true + + intelli-espower-loader@1.1.0: + resolution: {integrity: sha512-GmnpIM5tRU5n8R4bQAcu2gJMlfRukrtklbE1auRN8qGK9KSLboGdmHSLSLLnIHKrnRmgWRBXNqy5sIOrbT2l8g==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + invert-kv@1.0.0: + resolution: {integrity: sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==} + engines: {node: '>=0.10.0'} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ip-regex@2.1.0: + resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} + engines: {node: '>=4'} + + ip@1.1.9: + resolution: {integrity: sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-absolute-url@2.1.0: + resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==} + engines: {node: '>=0.10.0'} + + is-accessor-descriptor@1.0.2: + resolution: {integrity: sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g==} + engines: {node: '>= 0.4'} + + is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + + is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@1.0.1: + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-bluebird@1.0.2: + resolution: {integrity: sha512-PDRu1vVip5dGQg5tfn2qVCCyxbBYu5MhYUJwSfL/RoGBI97n1fxvilVazxzptZW0gcmsMH17H4EVZZI5E/RSeA==} + engines: {node: '>=0.10.0'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@1.2.1: + resolution: {integrity: sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==} + hasBin: true + + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + + is-class-hotfix@0.0.6: + resolution: {integrity: sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==} + + is-color-stop@1.1.0: + resolution: {integrity: sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + + is-descriptor@0.1.8: + resolution: {integrity: sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.4: + resolution: {integrity: sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==} + engines: {node: '>= 0.4'} + + is-directory@0.3.1: + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} + engines: {node: '>=0.10.0'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@1.0.0: + resolution: {integrity: sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-generator@1.0.3: + resolution: {integrity: sha512-G56jBpbJeg7ds83HW1LuShNs8J73Fv3CPz/bmROHOHlnKkN8sWb9ujiagjmxxMUywftgq48HlBZELKKqFLk0oA==} + + is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + + is-installed-globally@0.1.0: + resolution: {integrity: sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==} + engines: {node: '>=4'} + + is-installed-globally@0.3.2: + resolution: {integrity: sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==} + engines: {node: '>=8'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-keyword-js@1.0.3: + resolution: {integrity: sha512-EW8wNCNvomPa/jsH1g0DmLfPakkRCRTcTML1v1fZMLiVCvQ/1YB+tKsRzShBiWQhqrYCi5a+WsepA4Z8TA9iaA==} + engines: {node: '>=0.10.0'} + + is-lower-case@1.1.3: + resolution: {integrity: sha512-+5A1e/WJpLLXZEDlgz4G//WYSHyQBD32qa4Jd3Lw06qQlv3fJHnp3YIHjTQSGzHMgzmVKz2ZP3rBxTHkPw/lxA==} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-mobile@2.2.2: + resolution: {integrity: sha512-wW/SXnYJkTjs++tVK5b6kVITZpAZPtUrt9SF80vvxGiF/Oywal+COk1jlRkiVq15RFNEQKQY31TkV24/1T5cVg==} + + is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-npm@1.0.0: + resolution: {integrity: sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==} + engines: {node: '>=0.10.0'} + + is-npm@4.0.0: + resolution: {integrity: sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==} + engines: {node: '>=8'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@1.0.1: + resolution: {integrity: sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==} + engines: {node: '>=0.10.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-redirect@1.0.0: + resolution: {integrity: sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==} + engines: {node: '>=0.10.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-resolvable@1.1.0: + resolution: {integrity: sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==} + + is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + + is-type-of@1.4.0: + resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-upper-case@1.1.2: + resolution: {integrity: sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw==} + + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + + is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + + is-yarn-global@0.3.0: + resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.1: + resolution: {integrity: sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isomorphic-fetch@2.2.1: + resolution: {integrity: sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA==} + + isomorphic-style-loader@4.0.0: + resolution: {integrity: sha512-F+Sd5jnVbDLhd23c+ksYTj0TZztfCnF0hOZMVKy8ZUX+vVuFSYiNITWKifT8/odTlyRAVmQxO1Yl3CGf+jHKqw==} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + istanbul-lib-coverage@2.0.5: + resolution: {integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==} + engines: {node: '>=6'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@3.3.0: + resolution: {integrity: sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==} + engines: {node: '>=6'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@25.5.0: + resolution: {integrity: sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==} + engines: {node: '>= 8.3'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@2.2.1: + resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + + js-cookie@3.0.7: + resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==} + engines: {node: '>=20'} + + js-tokens@3.0.2: + resolution: {integrity: sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.13.1: + resolution: {integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==} + hasBin: true + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jsdoctypeparser@3.1.0: + resolution: {integrity: sha512-JNbkKpDFqbYjg+IU3FNo7qjX7Opy7CwjHywT32zgAcz/d4lX6Umn5jOHVETUdnNNgGrMk0nEx1gvP0F4M0hzlQ==} + engines: {node: '>=6'} + + jsdom@15.2.1: + resolution: {integrity: sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==} + engines: {node: '>=8'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@16.7.0: + resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} + engines: {node: '>=10'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + jsesc@1.3.0: + resolution: {integrity: sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==} + hasBin: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.3.1: + resolution: {integrity: sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + + json3@3.3.2: + resolution: {integrity: sha512-I5YLeauH3rIaE99EE++UeH2M2gSYo8/2TqDac7oZEH6D/DSQ4Woa628Qrfj1X9/OY5Mk5VvIDQaKCDchXaKrmA==} + deprecated: Please use the native JSON object instead of JSON 3 + + json5@0.5.1: + resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==} + hasBin: true + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jsonp-body@1.1.0: + resolution: {integrity: sha512-ZQyNWgHI8vvqclzz0l4/PatmeGureejrdMiLQGxTupMw0yQMHf+pQwS/7d5tpo2NBHsuwLCmXvZeNiz2RKdklA==} + engines: {node: '>= 0.10.0'} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + kcors@1.3.3: + resolution: {integrity: sha512-xuEWtIfnny0JDWhD4/Q6oXXCbaIXR9etDi/IK5bFSJSrlOb/9hKrJuu6O9vJdE9jthbHf1mHFzbyZh9+q8QpWw==} + engines: {node: '>= 0.12.9'} + + keygrip@1.1.0: + resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==} + engines: {node: '>= 0.6'} + + keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + klaw@1.3.1: + resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + known-css-properties@0.25.0: + resolution: {integrity: sha512-b0/9J1O9Jcyik1GC6KC42hJ41jKwdO/Mq8Mdo5sYN+IuRTXs2YFHZC3kZSx6ueusqa95x3wLYe/ytKjbAfGixA==} + + ko-lint-config@2.2.22: + resolution: {integrity: sha512-26QT6ia0BeJH9gKIuAdumP111orOEly8iPEEZq8CIFc43Q1uo946tGtjmXNa07ku0V9UAb6cg36QoKsoPbhk/g==} + engines: {node: '>=14'} + + ko-sleep@1.1.4: + resolution: {integrity: sha512-s05WGpvvzyTuRlRE8fM7ru2Z3O+InbJuBcckTWKg2W+2c1k6SnFa3IfiSSt0/peFrlYAXgNoxuJWWVNmWh+K/A==} + + koa-bodyparser@4.4.1: + resolution: {integrity: sha512-kBH3IYPMb+iAXnrxIhXnW+gXV8OTzCu8VPDqvcDHW9SQrbkHmqPQtiZwrltNmSq6/lpipHnT7k7PsjlVD7kK0w==} + engines: {node: '>=8.0.0'} + + koa-compose@2.5.1: + resolution: {integrity: sha512-bOGNe+7ZqVVCnGNej7qP8uJ+6ugiGLgpYgVnoj6KJAR8+rH0WzktzBXrRk/eHWisEYUa9I8ess0ROiVDfFXIOw==} + + koa-compose@3.2.1: + resolution: {integrity: sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw==} + + koa-compose@4.1.0: + resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==} + + koa-connect@1.0.0: + resolution: {integrity: sha512-NQ7H8gBh90LLi8Fq0i+gdS9ELYiu7Yplytsdh9xVYdKhjpcU82DOIq7h3recWHhQVwP1RWCAgJ76EVeMRe9RTw==} + + koa-connect@2.1.1: + resolution: {integrity: sha512-ejvbGKYS6di4LUSS+6E+Z5ZVev9RqThLm3NfZjb9QHZMASLvnr4eDTImKcGlQXFrtVpMTyTovZ+Hcl6JbBuFNA==} + + koa-convert@1.2.0: + resolution: {integrity: sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA==} + engines: {node: '>= 4'} + + koa-convert@2.0.0: + resolution: {integrity: sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==} + engines: {node: '>= 10'} + + koa-is-json@1.0.0: + resolution: {integrity: sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw==} + + koa-locales@1.12.0: + resolution: {integrity: sha512-lalx0OuvdZ39JppTVqHnYKIgJJbByon9xpt5KSKFOL6/VQS+XBMklMX83+fgkCXzsNacynWaN75ihWP2EGsWIQ==} + engines: {node: '>=4.0.0'} + + koa-onerror@4.2.0: + resolution: {integrity: sha512-D15tp5rxevHqqcvOiEDbtQolG6z3NpBNupz3EUZz43pjYv5SGMom2Xz1FKM8oTya56+aq+hejPW/iBrNnC/UGQ==} + engines: {node: '>= 8.0.0'} + + koa-override@3.0.0: + resolution: {integrity: sha512-w2rWCfapbQUZ8TrRBarj6iwryCTooEcdw9lr1hYC1q4FnaCZcAOhpjB1VpqtbODALVMgY3JGlzLSeYRXc5Ky0Q==} + engines: {node: '>= 8.0.0'} + + koa-range@0.3.0: + resolution: {integrity: sha512-Ich3pCz6RhtbajYXRWjIl6O5wtrLs6kE3nkXc9XmaWe+MysJyZO7K4L3oce1Jpg/iMgCbj+5UCiMm/rqVtcDIg==} + engines: {node: '>=7'} + + koa-session@6.4.0: + resolution: {integrity: sha512-h/dxmSOvNEXpHQPRs4TV03TZVFyZIjmYQiTAW5JBFTYBOZ0VdpZ8QEE6Dud75g8z9JNGXi3m++VqRmqToB+c2A==} + engines: {node: '>=8.0.0'} + + koa-static-cache@5.1.4: + resolution: {integrity: sha512-abVWOHY6z6qSTvNtapWMAnvHS9SUiUCaQQQubClSAT9ybQPsZ6ioKcRarnownS4fMD0sXQgQ5ey8CYEuwoa1Yg==} + engines: {node: '>= 7.6.0'} + + koa-webpack-hot-middleware@1.0.3: + resolution: {integrity: sha512-SKUW1S5nEZ6zrabc0svjWYE/jB3xtq8BYk0HrEmAyMZ4AxpCk4Cbq/t4dSWWSK19a6LrDDeZezVPmgb5xdgEaQ==} + + koa@1.7.1: + resolution: {integrity: sha512-OWunovXTG5xliG4iVQJ2YAGa7tSDHvJiRvjEEy5WMBpkFw/O54WneFB+efsCxJoT3pRdNapSZSMvvs4bUzbkIg==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + koa@2.16.4: + resolution: {integrity: sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==} + engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + last-call-webpack-plugin@3.0.0: + resolution: {integrity: sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==} + + latest-version@3.1.0: + resolution: {integrity: sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==} + engines: {node: '>=4'} + + latest-version@5.1.0: + resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} + engines: {node: '>=8'} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lcid@1.0.0: + resolution: {integrity: sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==} + engines: {node: '>=0.10.0'} + + less-loader@4.1.0: + resolution: {integrity: sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg==} + engines: {node: '>= 4.8 < 5.0.0 || >= 5.10'} + peerDependencies: + less: ^2.3.1 || ^3.0.0 + webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 + + less@3.9.0: + resolution: {integrity: sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w==} + engines: {node: '>=4'} + hasBin: true + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + livereload-js@3.4.1: + resolution: {integrity: sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==} + + livereload@0.9.3: + resolution: {integrity: sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==} + engines: {node: '>=8.0.0'} + hasBin: true + + load-json-file@1.1.0: + resolution: {integrity: sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==} + engines: {node: '>=0.10.0'} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + loader-fs-cache@1.0.3: + resolution: {integrity: sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==} + + loader-runner@2.4.0: + resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + + loader-utils@0.2.17: + resolution: {integrity: sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==} + + loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash._reinterpolate@3.0.0: + resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.clonedeepwith@4.5.0: + resolution: {integrity: sha512-QRBRSxhbtsX1nc0baxSkkK5WlVTTm/s48DSukcGcWZwIyI8Zz+lB+kFiELJXtzfH4Aj6kMWQ1VWW4U5uUDgZMA==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash.has@4.5.2: + resolution: {integrity: sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g==} + + lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + + lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.set@4.3.2: + resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash.template@4.18.1: + resolution: {integrity: sha512-5urZrLnV/VD6zHK5KsVtZgt7H19v51mIzoS0aBNH8yp3I8tbswrEjOABOPY8m8uB7NuibubLrMX+Y0PXsU9X+w==} + deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. + + lodash.templatesettings@4.2.0: + resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + + longest-streak@2.0.4: + resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} + + longest@2.0.1: + resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} + engines: {node: '>=0.10.0'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loud-rejection@1.6.0: + resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==} + engines: {node: '>=0.10.0'} + + lower-case-first@1.0.2: + resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} + + lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + + lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lowlight@1.20.0: + resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-queue@0.1.0: + resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + make-dir@1.3.0: + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@2.0.0: + resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} + engines: {node: '>=4'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + marked@1.2.9: + resolution: {integrity: sha512-H8lIX2SvyitGX+TRdtS06m1jHMijKN/XjfH6Ooii9fvxMlh8QdqBfBDkGUpMWH2kQNrtixjzYUa3SH8ROTgRRw==} + engines: {node: '>= 8.16.2'} + hasBin: true + + matcher@1.1.1: + resolution: {integrity: sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==} + engines: {node: '>=4'} + + material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + + mdast-util-definitions@4.0.0: + resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==} + + mdast-util-find-and-replace@1.1.1: + resolution: {integrity: sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==} + + mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + + mdast-util-gfm-autolink-literal@0.1.3: + resolution: {integrity: sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==} + + mdast-util-gfm-strikethrough@0.2.3: + resolution: {integrity: sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==} + + mdast-util-gfm-table@0.1.6: + resolution: {integrity: sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==} + + mdast-util-gfm-task-list-item@0.1.6: + resolution: {integrity: sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==} + + mdast-util-gfm@0.1.2: + resolution: {integrity: sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==} + + mdast-util-to-hast@10.2.0: + resolution: {integrity: sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==} + + mdast-util-to-markdown@0.6.5: + resolution: {integrity: sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==} + + mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + mdn-data@2.0.4: + resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + medium-zoom@1.1.0: + resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + + memoizee@0.4.17: + resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} + engines: {node: '>=0.12'} + + memory-fs@0.4.1: + resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==} + + memory-fs@0.5.0: + resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + meow@3.7.0: + resolution: {integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==} + engines: {node: '>=0.10.0'} + + meow@5.0.0: + resolution: {integrity: sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==} + engines: {node: '>=6'} + + meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + + meow@9.0.0: + resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==} + engines: {node: '>=10'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-estraverse-visitors@1.0.0: + resolution: {integrity: sha512-YcT59TImpdL2qe+I7OWI+ESjBVov9CWTQjK9Issk58BNQzyputg2s8wOE+DDvxtgmPHG4L6xAl0yAwbNCyXszg==} + + merge-source-map@1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + + method-override@2.3.10: + resolution: {integrity: sha512-Ks2/7e+3JuwQcpLybc6wTHyqg13HDjOhLcE+YaAEub9DbSxF+ieMvxUlybmWW9luRMh9Cd0rO9aNtzUT51xfNQ==} + engines: {node: '>= 0.8.0'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-extension-gfm-autolink-literal@0.5.7: + resolution: {integrity: sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==} + + micromark-extension-gfm-strikethrough@0.6.5: + resolution: {integrity: sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==} + + micromark-extension-gfm-table@0.4.3: + resolution: {integrity: sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==} + + micromark-extension-gfm-tagfilter@0.3.0: + resolution: {integrity: sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==} + + micromark-extension-gfm-task-list-item@0.3.3: + resolution: {integrity: sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==} + + micromark-extension-gfm@0.3.3: + resolution: {integrity: sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==} + + micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + + micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.3.4: + resolution: {integrity: sha512-sAaYXszED5ALBt665F0wMQCUXpGuZsGdopoqcHPdL39ZYdi7uHoZlhrfZfhv8WzivhBzr/oXwaj+yiK5wY8MXQ==} + hasBin: true + + mime@1.3.6: + resolution: {integrity: sha512-a/kG+3WTtU8GJG1ngpkkHOHcH6zNjGrI47OQyoFsFBN0QpYYJ4u2yEORsGK5cZMI+cfu9HbSCCfGfRzG0fWE9A==} + hasBin: true + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + mini-css-extract-plugin@0.9.0: + resolution: {integrity: sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.4.0 + + mini-store@2.0.0: + resolution: {integrity: sha512-EG0CuwpQmX+XL4QVS0kxNwHW5ftSbhygu1qxQH0pipugjnPkbvkalCdQbEihMwtQY6d3MTN+MS0q+aurs+RfLQ==} + + mini-store@3.0.6: + resolution: {integrity: sha512-YzffKHbYsMQGUWQRKdsearR79QsMzzJcDDmZKlJBqt5JNkqpyJHYlK6gP61O36X+sLf76sO9G6mhKBe83gIZIQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + + minimatch@3.0.4: + resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist-options@3.0.2: + resolution: {integrity: sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==} + engines: {node: '>= 4'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mississippi@2.0.0: + resolution: {integrity: sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==} + engines: {node: '>=4.0.0'} + + mississippi@3.0.0: + resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==} + engines: {node: '>=4.0.0'} + + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mkdirp@0.5.4: + resolution: {integrity: sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + hasBin: true + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mocha@6.2.3: + resolution: {integrity: sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg==} + engines: {node: '>= 6.0.0'} + hasBin: true + + mockjs@1.1.0: + resolution: {integrity: sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==} + hasBin: true + + modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + + moment-timezone@0.5.48: + resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + morgan@1.6.1: + resolution: {integrity: sha512-WWxlTx5xCqbtSeX/gPVHUZBhAhSMfYQLgPrWHEN0FYnF+zf1Ju/Zct6rpeKmvzibrYF4QvFVws7IN61BxnKu+Q==} + engines: {node: '>= 0.8.0'} + + move-concurrently@1.0.1: + resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} + deprecated: This package is no longer supported. + + ms@0.7.1: + resolution: {integrity: sha512-lRLiIR9fSNpnP6TC4v8+4OU7oStC01esuNowdQ34L+Gk8e5Puoc88IqJ+XAY/B3Mn2ZKis8l8HX90oU8ivzUHg==} + + ms@0.7.2: + resolution: {integrity: sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.1: + resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multi-stage-sourcemap@0.2.1: + resolution: {integrity: sha512-umaOM+8BZByZIB/ciD3dQLzTv50rEkkGJV78ta/tIVc/J/rfGZY5y1R+fBD3oTaolx41mK8rRcyGtYbDXlzx8Q==} + + multimatch@2.1.0: + resolution: {integrity: sha512-0mzK8ymiWdehTBiJh0vClAzGyQbdtyWqzSVx//EK4N/D+599RFlGfTAsKw2zMSABtDG9C6Ul2+t8f2Lbdjf5mA==} + engines: {node: '>=0.10.0'} + + multiparty@3.3.2: + resolution: {integrity: sha512-FX6dDOKzDpkrb5/+Imq+V6dmCZNnC02tMDiZfrgHSYgfQj6CVPGzOVqfbHKt/Vy4ZZsmMPXkulyLf92lCyvV7A==} + engines: {node: '>=0.8.0'} + + mustache@2.3.2: + resolution: {integrity: sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==} + engines: {npm: '>=1.4.0'} + hasBin: true + + mutation-observer@1.0.3: + resolution: {integrity: sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==} + + mutationobserver-shim@0.3.7: + resolution: {integrity: sha512-oRIDTyZQU96nAiz2AQyngwx1e89iApl2hN5AOYwyxLUB47UYsU3Wv9lJWqH5y/QdiYkc5HQLi23ZNB3fELdHcQ==} + + mute-stream@0.0.7: + resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mysql2@1.7.0: + resolution: {integrity: sha512-xTWWQPjP5rcrceZQ7CSTKR/4XIDeH/cRkNH/uzvVGQ7W5c7EJ0dXeJUusk7OKhIoHj7uFKUxDVSCfLIl+jluog==} + engines: {node: '>= 8.0'} + + mz-modules@1.0.0: + resolution: {integrity: sha512-BqeTNaLHfLScNF2HuZRaxKdLOBpGn3945A4ui2ZMwzzU+4bEc2GzqlKWoMqvpCc3V6dP/iRGDKr+rdfi74lsGw==} + engines: {node: '>=4.0.0'} + + mz-modules@2.1.0: + resolution: {integrity: sha512-sjk8lcRW3vrVYnZ+W+67L/2rL+jbO5K/N6PFGIcLWTiYytNr22Ah9FDXFs+AQntTM1boZcoHi5qS+CV1seuPog==} + engines: {node: '>=6.0.0'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + nan@2.27.0: + resolution: {integrity: sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + ndir@0.1.5: + resolution: {integrity: sha512-GEdViSiVHi63HASUGaQXNBbUNe5KDnuAYvOGBlY7y+CCCRT3NBeMWQlODb2GVV2cKwEIwHzo9KOCMsPBXlKsOg==} + engines: {node: '>= 0.4'} + + negotiator@0.5.3: + resolution: {integrity: sha512-oXmnazqehLNFohqgLxRyUdOQU9/UX0NpCpsnbjWUjM62ZM8oSOXYZpHc68XR130ftPNano0oQXGdREAplZRhaQ==} + engines: {node: '>= 0.6'} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nested-error-stacks@2.1.1: + resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-environment-flags@1.0.5: + resolution: {integrity: sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-fetch@1.7.3: + resolution: {integrity: sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-glob@1.2.0: + resolution: {integrity: sha512-c0R4Wab2SAlwdBr5ehPANnbLzxv5dBMUdEYy8ilqBDkqvEIf74JGhaLhCh/EuhgzPTXuEOUoqDnAKdODpHXMNg==} + + node-hex@1.0.1: + resolution: {integrity: sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==} + engines: {node: '>=8.0.0'} + + node-homedir@1.1.1: + resolution: {integrity: sha512-Xsmf94D/DdeDISAECUaxXVxhh+kHdbOQE4CnP4igo3HXL3BSmmUpD5M7orH434EZZwBTFF2xe5SgsQr/wOBuNw==} + engines: {node: '>=4.0.0'} + + node-http-server@8.1.6: + resolution: {integrity: sha512-D115pVWCh1aVVlB/eq37t2npNFdB5qT8B6mziXT9zf/r4df6r2kd6qUHd6jY94uwAOATzNTnl9eLvt1CfWjuAw==} + engines: {node: '>=6.5.0'} + hasBin: true + + node-libs-browser@2.2.1: + resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==} + + node-nightly-version@1.0.6: + resolution: {integrity: sha512-X+sfEj8oE4aEFNzXJQB2meO5qUTOzQ+c5gkhaxIXyei4AWYtjkrLrKVLNbylNzl2e7czkDFKDF4ajDy4n+yxtQ==} + engines: {node: '>=4'} + hasBin: true + + node-nightly-versions@1.0.2: + resolution: {integrity: sha512-kgqiljwMNsKjSOYOJO0fobx3fQ46Jije0SgSE33svk2KcS/KiaCNdYM81dnjL3mOMDCSdT6ZtYjQnBobxFOw8Q==} + engines: {node: '>=4'} + + node-noop@1.0.0: + resolution: {integrity: sha512-1lpWqKwZ9yUosQfW1uy3jm6St4ZbmeDKKGmdzwzedbyBI4LgHtGyL1ofDdqiSomgaYaSERi+qWtj64huJQjl7g==} + + node-releases@2.0.45: + resolution: {integrity: sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==} + engines: {node: '>=18'} + + node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} + + node-ssh@10.0.2: + resolution: {integrity: sha512-9sWHeIirOCI/z7RsM+eYiTsFhCr1+3+AQOTUMJ2hRR1bLlcEVjZuqaYS0AxAV2e7aV1meORG6EvCSPIqSjQ1Zg==} + engines: {node: '>= 8'} + + node-ssh@6.0.0: + resolution: {integrity: sha512-R/nqH09/kZrXilSLizJgokw4hJTSADANnntjk6iNVfO95RPNQq9H19NeUvQof96WQB9dgOxpRgBV7d+snZP2DQ==} + + node-tool-utils@1.6.0: + resolution: {integrity: sha512-nMnQo2r3cR/UMLiXo8DsFTKWnHiwirAWk1IjnC+lJ/nD7q7wiwVSGp0sWZ7XDUr37GmEwAFE58XlHLy7sitQpg==} + engines: {node: '>=6.0.0'} + + nodeinstall@0.1.6: + resolution: {integrity: sha512-D16seOJ69s755r4YBX3H8BHMzuqRmTAM0g7nHIXqhoi4jV55OMOHkqYiKrlCtUXFUvCh3Nf15cZCt/UC3c+sww==} + engines: {node: '>=4.0.0'} + hasBin: true + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@1.9.1: + resolution: {integrity: sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==} + engines: {node: '>=4'} + + normalize-url@3.3.0: + resolution: {integrity: sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==} + engines: {node: '>=6'} + + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + + notepack.io@2.2.0: + resolution: {integrity: sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw==} + + npm-install-webpack-plugin@4.0.5: + resolution: {integrity: sha512-1TWQzMuYFsb/+ZLDVb665Ayc+Fn399bw28WusEEJgoUWnR23FtqYk48ujqWJ3Tcsi3LAJJffuvkSLb3BrbmrTA==} + engines: {node: '>=4.2.0'} + peerDependencies: + webpack: ^1.0.0 || ^2.0.0 || >= 3.0.0-rc.0 || ^3.0.0 + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nth-check@1.0.2: + resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + num2fraction@1.2.2: + resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} + + number-is-nan@1.0.1: + resolution: {integrity: sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==} + engines: {node: '>=0.10.0'} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + nyc@13.3.0: + resolution: {integrity: sha512-P+FwIuro2aFG6B0Esd9ZDWUd51uZrAEoGutqZxzrVmYl3qSfkLgcQpBPBjtDFsUQLFY1dvTQJPOyeqr8S9GF8w==} + engines: {node: '>=6'} + hasBin: true + bundledDependencies: + - archy + - arrify + - caching-transform + - convert-source-map + - find-cache-dir + - find-up + - foreground-child + - glob + - istanbul-lib-coverage + - istanbul-lib-hook + - istanbul-lib-report + - istanbul-lib-source-maps + - istanbul-reports + - make-dir + - merge-source-map + - resolve-from + - rimraf + - signal-exit + - spawn-wrap + - test-exclude + - uuid + - yargs + - yargs-parser + + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-component@0.0.3: + resolution: {integrity: sha512-S0sN3agnVh2SZNEIGc0N1X4Z5K0JeFbGBrnuZpsxuUh5XLF0BnvWkMjRXo/zGKLd/eghvNIKcx1pQkmUjXIyrA==} + + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + + object-hash@1.3.1: + resolution: {integrity: sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==} + engines: {node: '>= 0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + + object.assign@4.1.0: + resolution: {integrity: sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.getownpropertydescriptors@2.1.9: + resolution: {integrity: sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.hasown@1.1.4: + resolution: {integrity: sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==} + engines: {node: '>= 0.4'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + omit.js@1.0.2: + resolution: {integrity: sha512-/QPc6G2NS+8d4L/cQhbk6Yit1WTB6Us2g84A7A/1+w9d/eRGHyEqC5kkQtHVoHZ5NFWGG7tUGgrhVZwgZanKrQ==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + only@0.0.2: + resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==} + + open@6.4.0: + resolution: {integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==} + engines: {node: '>=8'} + + opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + opn@5.5.0: + resolution: {integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==} + engines: {node: '>=4'} + + optimize-css-assets-webpack-plugin@5.0.8: + resolution: {integrity: sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q==} + peerDependencies: + webpack: ^4.0.0 + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + options@0.0.6: + resolution: {integrity: sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==} + engines: {node: '>=0.4.0'} + + opts@2.0.2: + resolution: {integrity: sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==} + + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + + os-homedir@1.0.2: + resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} + engines: {node: '>=0.10.0'} + + os-locale@1.4.0: + resolution: {integrity: sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==} + engines: {node: '>=0.10.0'} + + os-name@1.0.3: + resolution: {integrity: sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==} + engines: {node: '>=0.10.0'} + hasBin: true + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + osx-release@1.1.0: + resolution: {integrity: sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==} + engines: {node: '>=0.10.0'} + hasBin: true + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + + p-event@4.2.0: + resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==} + engines: {node: '>=8'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-finally@2.0.1: + resolution: {integrity: sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==} + engines: {node: '>=8'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@1.2.0: + resolution: {integrity: sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==} + engines: {node: '>=4'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@4.1.0: + resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==} + engines: {node: '>=10'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-json@4.0.1: + resolution: {integrity: sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==} + engines: {node: '>=4'} + + package-json@6.5.0: + resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} + engines: {node: '>=8'} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parallel-transform@1.2.0: + resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} + + param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + + parameter@2.4.0: + resolution: {integrity: sha512-PxENY9SO4qYIBRUXJ7dM/nWxbzC4PIDws1U7IQsZ0tlZvrcuAKxJQZV6Jf0Lmpl1mSYaEPcvhMOL2UHtLU2Xtw==} + engines: {node: '>= 4.0.0'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parent-require@1.0.0: + resolution: {integrity: sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==} + engines: {node: '>= 0.4.0'} + + parse-asn1@5.1.9: + resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} + engines: {node: '>= 0.10'} + + parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + + parse-json@2.2.0: + resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} + engines: {node: '>=0.10.0'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@5.1.0: + resolution: {integrity: sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parsejson@0.0.3: + resolution: {integrity: sha512-v38ZjVbinlZ2r1Rz06WUZEnGoSRcEGX+roMsiWjHeAe23s2qlQUyfmsPQZvh7d8l0E8AZzTIO/RkUr00LfkSiA==} + + parseqs@0.0.5: + resolution: {integrity: sha512-B3Nrjw2aL7aI4TDujOzfA4NsEc4u1lVcIRE0xesutH8kjeWF70uk+W5cBlIQx04zUH9NTBvuN36Y9xLRPK6Jjw==} + + parseqs@0.0.6: + resolution: {integrity: sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==} + + parseuri@0.0.5: + resolution: {integrity: sha512-ijhdxJu6l5Ru12jF0JvzXVPvsC+VibqeaExlNoMhWN6VQ79PGjkmc7oA4W1lp00sFkNyj0fx6ivPLdV51/UMog==} + + parseuri@0.0.6: + resolution: {integrity: sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@2.0.1: + resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + + path-browserify@0.0.1: + resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} + + path-case@2.1.1: + resolution: {integrity: sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==} + + path-dirname@1.0.2: + resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} + + path-exists@2.1.0: + resolution: {integrity: sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==} + engines: {node: '>=0.10.0'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@1.1.0: + resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} + engines: {node: '>=0.10.0'} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + pause@0.1.0: + resolution: {integrity: sha512-aeHLgQCtI3tcuYVnrvAeVb4Tkm1za4r3YDv3hMeUxcRxet3dbEhJOdtoMrsT/Q5tY3Oy2A1A9FD5el5tWp2FSg==} + engines: {node: '>= 0.6'} + + pbkdf2@3.1.5: + resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} + engines: {node: '>= 0.10'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-dir@1.0.0: + resolution: {integrity: sha512-c6pv3OE78mcZ92ckebVDqg0aWSoKhOTbwCV6qbCWMk546mAL9pZln0+QsN/yQ7fkucd4+yJPLrCBXNt8Ruk+Eg==} + engines: {node: '>=0.10.0'} + + pkg-dir@2.0.0: + resolution: {integrity: sha512-ojakdnUgL5pzJYWw2AIDEupaQCX5OPbM688ZevubICjdIX01PRSYKqm33fJoCOJBRseYCTUlQRnBNX+Pchaejw==} + engines: {node: '>=4'} + + pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + + please-upgrade-node@3.2.0: + resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} + + pluralize@7.0.0: + resolution: {integrity: sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==} + engines: {node: '>=4'} + + pn@1.1.0: + resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==} + + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-calc@7.0.5: + resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==} + + postcss-colormin@4.0.3: + resolution: {integrity: sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==} + engines: {node: '>=6.9.0'} + + postcss-convert-values@4.0.1: + resolution: {integrity: sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==} + engines: {node: '>=6.9.0'} + + postcss-discard-comments@4.0.2: + resolution: {integrity: sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==} + engines: {node: '>=6.9.0'} + + postcss-discard-duplicates@4.0.2: + resolution: {integrity: sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==} + engines: {node: '>=6.9.0'} + + postcss-discard-empty@4.0.1: + resolution: {integrity: sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==} + engines: {node: '>=6.9.0'} + + postcss-discard-overridden@4.0.1: + resolution: {integrity: sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==} + engines: {node: '>=6.9.0'} + + postcss-less@6.0.0: + resolution: {integrity: sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==} + engines: {node: '>=12'} + peerDependencies: + postcss: ^8.3.5 + + postcss-load-config@2.1.2: + resolution: {integrity: sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==} + engines: {node: '>= 4'} + + postcss-loader@3.0.0: + resolution: {integrity: sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==} + engines: {node: '>= 6'} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-merge-longhand@4.0.11: + resolution: {integrity: sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==} + engines: {node: '>=6.9.0'} + + postcss-merge-rules@4.0.3: + resolution: {integrity: sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==} + engines: {node: '>=6.9.0'} + + postcss-minify-font-values@4.0.2: + resolution: {integrity: sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==} + engines: {node: '>=6.9.0'} + + postcss-minify-gradients@4.0.2: + resolution: {integrity: sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==} + engines: {node: '>=6.9.0'} + + postcss-minify-params@4.0.2: + resolution: {integrity: sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==} + engines: {node: '>=6.9.0'} + + postcss-minify-selectors@4.0.2: + resolution: {integrity: sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==} + engines: {node: '>=6.9.0'} + + postcss-modules-extract-imports@2.0.0: + resolution: {integrity: sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==} + engines: {node: '>= 6'} + + postcss-modules-local-by-default@3.0.3: + resolution: {integrity: sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==} + engines: {node: '>= 6'} + + postcss-modules-scope@2.2.0: + resolution: {integrity: sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==} + engines: {node: '>= 6'} + + postcss-modules-values@3.0.0: + resolution: {integrity: sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==} + + postcss-normalize-charset@4.0.1: + resolution: {integrity: sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==} + engines: {node: '>=6.9.0'} + + postcss-normalize-display-values@4.0.2: + resolution: {integrity: sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==} + engines: {node: '>=6.9.0'} + + postcss-normalize-positions@4.0.2: + resolution: {integrity: sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-repeat-style@4.0.2: + resolution: {integrity: sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==} + engines: {node: '>=6.9.0'} + + postcss-normalize-string@4.0.2: + resolution: {integrity: sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-timing-functions@4.0.2: + resolution: {integrity: sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==} + engines: {node: '>=6.9.0'} + + postcss-normalize-unicode@4.0.1: + resolution: {integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==} + engines: {node: '>=6.9.0'} + + postcss-normalize-url@4.0.1: + resolution: {integrity: sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==} + engines: {node: '>=6.9.0'} + + postcss-normalize-whitespace@4.0.2: + resolution: {integrity: sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==} + engines: {node: '>=6.9.0'} + + postcss-ordered-values@4.1.2: + resolution: {integrity: sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==} + engines: {node: '>=6.9.0'} + + postcss-reduce-initial@4.0.3: + resolution: {integrity: sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==} + engines: {node: '>=6.9.0'} + + postcss-reduce-transforms@4.0.2: + resolution: {integrity: sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==} + engines: {node: '>=6.9.0'} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-scss@4.0.4: + resolution: {integrity: sha512-aBBbVyzA8b3hUL0MGrpydxxXKXFZc5Eqva0Q3V9qsBOLEMsjb6w49WfpsoWzpEgcqJGW4t7Rio8WXVU9Gd8vWg==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-selector-parser@3.1.2: + resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==} + engines: {node: '>=8'} + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-sorting@7.0.1: + resolution: {integrity: sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==} + peerDependencies: + postcss: ^8.3.9 + + postcss-svgo@4.0.3: + resolution: {integrity: sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==} + engines: {node: '>=6.9.0'} + + postcss-unique-selectors@4.0.1: + resolution: {integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==} + engines: {node: '>=6.9.0'} + + postcss-value-parser@3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + + postcss@8.4.14: + resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + power-assert-context-formatter@1.2.0: + resolution: {integrity: sha512-HLNEW8Bin+BFCpk/zbyKwkEu9W8/zThIStxGo7weYcFkKgMuGCHUJhvJeBGXDZf0Qm2xis4pbnnciGZiX0EpSg==} + + power-assert-context-reducer-ast@1.2.0: + resolution: {integrity: sha512-EgOxmZ/Lb7tw4EwSKX7ZnfC0P/qRZFEG28dx/690qvhmOJ6hgThYFm5TUWANDLK5NiNKlPBi5WekVGd2+5wPrw==} + + power-assert-context-traversal@1.2.0: + resolution: {integrity: sha512-NFoHU6g2umNajiP2l4qb0BRWD773Aw9uWdWYH9EQsVwIZnog5bd2YYLFCVvaxWpwNzWeEfZIon2xtyc63026pQ==} + + power-assert-formatter@1.4.1: + resolution: {integrity: sha512-c2QzTk1a6BUumuzjffFUrsMlx2gqLEoeEMrx6gVaHzQ/zTBTibQGblaQslbv72eq9RJNFQXRryjTHoffIEz+ww==} + + power-assert-renderer-assertion@1.2.0: + resolution: {integrity: sha512-3F7Q1ZLmV2ZCQv7aV7NJLNK9G7QsostrhOU7U0RhEQS/0vhEqrRg2jEJl1jtUL4ZyL2dXUlaaqrmPv5r9kRvIg==} + + power-assert-renderer-base@1.1.1: + resolution: {integrity: sha512-aGCUi0NuNd/fVS6KKMLTjRP58cdlHlQKgXV4WKl3YlUhnN0d9QBEYOyvmiumdjk+5GuZmozvEmBIcTAcxEZqnw==} + + power-assert-renderer-comparison@1.2.0: + resolution: {integrity: sha512-7c3RKPDBKK4E3JqdPtYRE9cM8AyX4LC4yfTvvTYyx8zSqmT5kJnXwzR0yWQLOavACllZfwrAGQzFiXPc5sWa+g==} + + power-assert-renderer-diagram@1.2.0: + resolution: {integrity: sha512-JZ6PC+DJPQqfU6dwSmpcoD7gNnb/5U77bU5KgNwPPa+i1Pxiz6UuDeM3EUBlhZ1HvH9tMjI60anqVyi5l2oNdg==} + + power-assert-renderer-file@1.2.0: + resolution: {integrity: sha512-/oaVrRbeOtGoyyd7e4IdLP/jIIUFJdqJtsYzP9/88R39CMnfF/S/rUc8ZQalENfUfQ/wQHu+XZYRMaCEZmEesg==} + + power-assert-util-string-width@1.2.0: + resolution: {integrity: sha512-lX90G0igAW0iyORTILZ/QjZWsa1MZ6VVY3L0K86e2eKun3S4LKPH4xZIl8fdeMYLfOjkaszbNSzf1uugLeAm2A==} + + power-assert@1.6.1: + resolution: {integrity: sha512-VWkkZV6Y+W8qLX/PtJu2Ur2jDPIs0a5vbP0TpKeybNcIXmT4vcKoVkyTp5lnQvTpY/DxacAZ4RZisHRHLJcAZQ==} + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prepend-http@1.0.4: + resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} + engines: {node: '>=0.10.0'} + + prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@2.7.1: + resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} + engines: {node: '>=10.13.0'} + hasBin: true + + pretty-bytes@4.0.2: + resolution: {integrity: sha512-yJAF+AjbHKlxQ8eezMd/34Mnj/YTQ3i6kLzvVsH4l/BfIFtp444n0wVbnsn66JimZ9uBofv815aRp1zCppxlWw==} + engines: {node: '>=4'} + + pretty-error@2.1.2: + resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==} + + prismjs@1.27.0: + resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==} + engines: {node: '>=6'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + private@0.1.8: + resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==} + engines: {node: '>= 0.6'} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress-bar-webpack-plugin@1.12.1: + resolution: {integrity: sha512-tVbPB5xBbqNwdH3mwcxzjL1r1Vrm/xGu93OsqVSAbCaXGoKFvfWIh0gpMDpn2kYsPVRSAIK0pBkP9Vfs+JJibQ==} + peerDependencies: + webpack: ^1.3.0 || ^2 || ^3 || ^4 + + progress@1.1.8: + resolution: {integrity: sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==} + engines: {node: '>=0.4.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@5.6.0: + resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + + pump@2.0.1: + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pumpify@1.5.1: + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pupa@2.1.1: + resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} + engines: {node: '>=8'} + + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + + qs@4.0.0: + resolution: {integrity: sha512-8MPmJ83uBOPsQj5tQCv4g04/nTiY+d17yl9o3Bw73vC6XlEm2POIRRlOgWJ8i74bkGLII670cDJJZkgiZ2sIkg==} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + qs@6.5.5: + resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} + engines: {node: '>=0.6'} + + query-string@4.3.4: + resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==} + engines: {node: '>=0.10.0'} + + querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@1.1.0: + resolution: {integrity: sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==} + engines: {node: '>=4'} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + + range-parser@1.0.3: + resolution: {integrity: sha512-nDsRrtIxVUO5opg/A8T2S3ebULVIfuh8ECbh4w3N4mWxIiT3QILDJDUQayPqm2e8Q8NUa0RSUkGCfe33AfjR3Q==} + engines: {node: '>= 0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.1.7: + resolution: {integrity: sha512-x4d27vsIG04gZ1imkuDXB9Rd/EkAx5kYzeMijIYw1PAor0Ld3nTlkQQwDjKu42GdRUFCX1AfGnTSQB4O57eWVg==} + engines: {node: '>= 0.8'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc-align@2.4.5: + resolution: {integrity: sha512-nv9wYUYdfyfK+qskThf4BQUSIadeI/dCsfaMZfNEoxm9HwOIioQ+LyqmMK6jWHAZQgOzMLaqawhuBXlF63vgjw==} + + rc-align@4.0.15: + resolution: {integrity: sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-animate@2.11.1: + resolution: {integrity: sha512-1NyuCGFJG/0Y+9RKh5y/i/AalUCA51opyyS/jO2seELpgymZm2u9QV3xwODwEuzkmeQ1BDPxMLmYLcTJedPlkQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-animate@3.1.1: + resolution: {integrity: sha512-8wg2Zg3EETy0k/9kYuis30NJNQg1D6/WSQwnCiz6SvyxQXNet/rVraRz3bPngwY6rcU2nlRvoShiYOorXyF7Sg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-calendar@9.15.11: + resolution: {integrity: sha512-qv0VXfAAnysMWJigxaP6se4bJHvr17D9qsLbi8BOpdgEocsS0RkgY1IUiFaOVYKJDy/EyLC447O02sV/y5YYBg==} + + rc-cascader@0.17.5: + resolution: {integrity: sha512-WYMVcxU0+Lj+xLr4YYH0+yXODumvNXDcVEs5i7L1mtpWwYkubPV/zbQpn+jGKFCIW/hOhjkU4J1db8/P/UKE7A==} + + rc-cascader@1.4.3: + resolution: {integrity: sha512-Q4l9Mv8aaISJ+giVnM9IaXxDeMqHUGLvi4F+LksS6pHlaKlN4awop/L+IMjIXpL+ug/ojaCyv/ixcVopJYYCVA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-checkbox@2.1.8: + resolution: {integrity: sha512-6qOgh0/by0nVNASx6LZnhRTy17Etcgav+IrI7kL9V9kcDZ/g7K14JFlqrtJ3NjDq/Kyn+BPI1st1XvbkhfaJeg==} + + rc-checkbox@2.3.2: + resolution: {integrity: sha512-afVi1FYiGv1U0JlpNH/UaEXdh6WUJjcWokj/nUN2TgG80bfG+MDdbfHKlLcNNba94mbjy2/SXJ1HDgrOkXGAjg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-collapse@1.11.8: + resolution: {integrity: sha512-8EhfPyScTYljkbRuIoHniSwZagD5UPpZ3CToYgoNYWC85L2qCbPYF7+OaC713FOrIkp6NbfNqXsITNxmDAmxog==} + + rc-collapse@3.1.4: + resolution: {integrity: sha512-WayrhswKMwuJab9xbqFxXTgV0m6X8uOPEO6zm/GJ5YJiJ/wIh/Dd2VtWeI06HYUEnTFv0HNcYv+zWbB+p6OD2A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dialog@7.6.1: + resolution: {integrity: sha512-KUKf+2eZ4YL+lnXMG3hR4ZtIhC9glfH27NtTVz3gcoDIPAf3uUvaXVRNoDCiSi+OGKLyIb/b6EoidFh6nQC5Wg==} + + rc-dialog@8.5.3: + resolution: {integrity: sha512-zoamT8L6+rBwnwjPlrZRxiHCHQXrTcWZD3a6ruoqEdUKP1KgO0eSjMDH9WlF3WEPYMVnb2G5SrjHrhnwgPDu5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dialog@8.6.0: + resolution: {integrity: sha512-GSbkfqjqxpZC5/zc+8H332+q5l/DKUhpQr0vdX2uDsxo5K0PhvaMEVjyoJUTkZ3+JstEADQji1PVLVb/2bJeOQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-drawer@3.1.3: + resolution: {integrity: sha512-2z+RdxmzXyZde/1OhVMfDR1e/GBswFeWSZ7FS3Fdd0qhgVdpV1wSzILzzxRaT481ItB5hOV+e8pZT07vdJE8kg==} + peerDependencies: + react: '*' + + rc-drawer@4.3.1: + resolution: {integrity: sha512-GMfFy4maqxS9faYXEhQ+0cA1xtkddEQzraf6SAdzWbn444DrrLogwYPk1NXSpdXjLCLxgxOj9MYtyYG42JsfXg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dropdown@2.4.1: + resolution: {integrity: sha512-p0XYn0wrOpAZ2fUGE6YJ6U8JBNc5ASijznZ6dkojdaEfQJAeZtV9KMEewhxkVlxGSbbdXe10ptjBlTEW9vEwEg==} + + rc-dropdown@3.2.5: + resolution: {integrity: sha512-dVO2eulOSbEf+F4OyhCY5iGiMVhUYY/qeXxL7Ex2jDBt/xc89jU07mNoowV6aWxwVOc70pxEINff0oM2ogjluA==} + peerDependencies: + react: '*' + react-dom: '*' + + rc-editor-core@0.8.10: + resolution: {integrity: sha512-T3aHpeMCIYA1sdAI7ynHHjXy5fqp83uPlD68ovZ0oClTSc3tbHmyCxXlA+Ti4YgmcpCYv7avF6a+TIbAka53kw==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + + rc-editor-mention@1.1.13: + resolution: {integrity: sha512-3AOmGir91Fi2ogfRRaXLtqlNuIwQpvla7oUnGHS1+3eo7b+fUp5IlKcagqtwUBB5oDNofoySXkLBxzWvSYNp/Q==} + peerDependencies: + react: '>=15.x' + react-dom: '>=15.x' + + rc-field-form@1.20.1: + resolution: {integrity: sha512-f64KEZop7zSlrG4ef/PLlH12SLn6iHDQ3sTG+RfKBM45hikwV1i8qMf53xoX12NvXXWg1VwchggX/FSso4bWaA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>= 16.9.0' + react-dom: '>= 16.9.0' + + rc-form@2.4.12: + resolution: {integrity: sha512-sHfyWRrnjCHkeCYfYAGop2GQBUC6CKMPcJF9h/gL/vTmZB/RN6fNOGKjXrXjFbwFwKXUWBoPtIDDDmXQW9xNdw==} + peerDependencies: + prop-types: ^15.0 + + rc-hammerjs@0.6.10: + resolution: {integrity: sha512-Vgh9qIudyN5CHRop4M+v+xUniQBFWXKrsJxQRVtJOi2xgRrCeI52/bkpaL5HWwUhqTK9Ayq0n7lYTItT6ld5rg==} + + rc-image@5.2.5: + resolution: {integrity: sha512-qUfZjYIODxO0c8a8P5GeuclYXZjzW4hV/5hyo27XqSFo1DmTCs2HkVeQObkcIk5kNsJtgsj1KoPThVsSc/PXOw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-input-number@4.5.9: + resolution: {integrity: sha512-wAT4EBpLDW4+27c935k4F1JLk+gnhyGBkpzBmtkNvIHLG8yTndZSJ2bFfSYfkA6C82IxmAztXs3ffCeUd/rkbg==} + + rc-input-number@7.1.4: + resolution: {integrity: sha512-EG4iqkqyqzLRu/Dq+fw2od7nlgvXLEatE+J6uhi3HXE1qlM3C7L6a7o/hL9Ly9nimkES2IeQoj3Qda3I0izj3Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-mentions@0.4.2: + resolution: {integrity: sha512-DTZurQzacLXOfVuiHydGzqkq7cFMHXF18l2jZ9PhWUn2cqvOSY3W4osN0Pq29AOMOBpcxdZCzgc7Lb0r/bgkDw==} + peerDependencies: + react: '*' + + rc-mentions@1.5.3: + resolution: {integrity: sha512-NG/KB8YiKBCJPHHvr/QapAb4f9YzLJn7kDHtmI1K6t7ZMM5YgrjIxNNhoRKKP9zJvb9PdPts69Hbg4ZMvLVIFQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-menu@7.5.5: + resolution: {integrity: sha512-4YJXJgrpUGEA1rMftXN7bDhrV5rPB8oBJoHqT+GVXtIWCanfQxEnM3fmhHQhatL59JoAFMZhJaNzhJIk4FUWCQ==} + peerDependencies: + react: '*' + react-dom: '*' + + rc-menu@8.10.8: + resolution: {integrity: sha512-0gnSR0nmR/60NnK+72EGd+QheHyPSQ3wYg1TwX1zl0JJ9Gm0purFFykCXVv/G0Jynpt0QySPAos+bpHpjMZdoQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-motion@2.9.5: + resolution: {integrity: sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-notification@3.3.1: + resolution: {integrity: sha512-U5+f4BmBVfMSf3OHSLyRagsJ74yKwlrQAtbbL5ijoA0F2C60BufwnOcHG18tVprd7iaIjzZt1TKMmQSYSvgrig==} + + rc-notification@4.5.7: + resolution: {integrity: sha512-zhTGUjBIItbx96SiRu3KVURcLOydLUHZCPpYEn1zvh+re//Tnq/wSxN4FKgp38n4HOgHSVxcLEeSxBMTeBBDdw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-overflow@1.5.0: + resolution: {integrity: sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-pagination@1.20.15: + resolution: {integrity: sha512-/Xr4/3GOa1DtL8iCYl7qRUroEMrRDhZiiuHwcVFfSiwa9LYloMlUWcOJsnr8LN6A7rLPdm3/CHStUNeYd+2pKw==} + + rc-pagination@3.1.17: + resolution: {integrity: sha512-/BQ5UxcBnW28vFAcP2hfh+Xg15W0QZn8TWYwdCApchMH1H0CxiaUUcULP8uXcFM1TygcdKWdt3JqsL9cTAfdkQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-picker@2.5.19: + resolution: {integrity: sha512-u6myoCu/qiQ0vLbNzSzNrzTQhs7mldArCpPHrEI6OUiifs+IPXmbesqSm0zilJjfzrZJLgYeyyOMSznSlh0GKA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-progress@2.5.3: + resolution: {integrity: sha512-K2fa4CnqGehLZoMrdmBeZ86ONSTVcdk5FlqetbwJ3R/+42XfqhwQVOjWp2MH4P7XSQOMAGcNOy1SFfCP3415sg==} + + rc-progress@3.1.4: + resolution: {integrity: sha512-XBAif08eunHssGeIdxMXOmRQRULdHaDdIFENQ578CMb4dyewahmmfJRyab+hw4KH4XssEzzYOkAInTLS7JJG+Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-rate@2.5.1: + resolution: {integrity: sha512-3iJkNJT8xlHklPCdeZtUZmJmRVUbr6AHRlfSsztfYTXVlHrv2TcPn3XkHsH+12j812WVB7gvilS2j3+ffjUHXg==} + + rc-rate@2.9.3: + resolution: {integrity: sha512-2THssUSnRhtqIouQIIXqsZGzRczvp4WsH4WvGuhiwm+LG2fVpDUJliP9O1zeDOZvYfBE/Bup4SgHun/eCkbjgQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-resize-observer@0.1.3: + resolution: {integrity: sha512-uzOQEwx83xdQSFOkOAM7x7GHIQKYnrDV4dWxtCxyG1BS1pkfJ4EvDeMfsvAJHSYkQXVBu+sgRHGbRtLG3qiuUg==} + peerDependencies: + react: ^16.0.0 + react-dom: ^16.0.0 + + rc-resize-observer@1.4.3: + resolution: {integrity: sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-select@12.1.13: + resolution: {integrity: sha512-cPI+aesP6dgCAaey4t4upDbEukJe+XN0DK6oO/6flcCX5o28o7KNZD7JAiVtC/6fCwqwI/kSs7S/43dvHmBl+A==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-select@9.2.3: + resolution: {integrity: sha512-WhswxOMWiNnkXRbxyrj0kiIvyCfo/BaRPaYbsDetSIAU2yEDwKHF798blCP5u86KLOBKBvtxWLFCkSsQw1so5w==} + + rc-slider@8.7.1: + resolution: {integrity: sha512-WMT5mRFUEcrLWwTxsyS8jYmlaMsTVCZIGENLikHsNv+tE8ThU2lCoPfi/xFNUfJFNFSBFP3MwPez9ZsJmNp13g==} + + rc-slider@9.7.5: + resolution: {integrity: sha512-LV/MWcXFjco1epPbdw1JlLXlTgmWpB9/Y/P2yinf8Pg3wElHxA9uajN21lJiWtZjf5SCUekfSP6QMJfDo4t1hg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-steps@3.5.0: + resolution: {integrity: sha512-2Vkkrpa7PZbg7qPsqTNzVDov4u78cmxofjjnIHiGB9+9rqKS8oTLPzbW2uiWDr3Lk+yGwh8rbpGO1E6VAgBCOg==} + + rc-steps@4.1.4: + resolution: {integrity: sha512-qoCqKZWSpkh/b03ASGx1WhpKnuZcRWmvuW+ZUu4mvMdfvFzVxblTwUM+9aBd0mlEUFmt6GW8FXhMpHkK3Uzp3w==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-switch@1.9.2: + resolution: {integrity: sha512-qaK7mY4FLDKy99Hq3A1tf8CcqfzKtHp9LPX8WTnZ0MzdHCTneSARb1XD7Eqeu8BactasYGsi2bF9p18Q+/5JEw==} + peerDependencies: + react: ^16.0.0 + react-dom: ^16.0.0 + + rc-switch@3.2.2: + resolution: {integrity: sha512-+gUJClsZZzvAHGy1vZfnwySxj+MjLlGRyXKXScrtCTcmiYNPzxDFOxdQ/3pK1Kt/0POvwJ/6ALOR8gwdXGhs+A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-table@6.10.15: + resolution: {integrity: sha512-LAr0M/gqt+irOjvPNBLApmQ0CUHNOfKsEBhu1uIuB3OlN1ynA9z+sdoTQyNd9+8NSl0MYnQOOfhtLChAY7nU0A==} + peerDependencies: + react: ^16.0.0 + react-dom: ^16.0.0 + + rc-table@7.13.3: + resolution: {integrity: sha512-oP4fknjvKCZAaiDnvj+yzBaWcg+JYjkASbeWonU1BbrLcomkpKvMUgPODNEzg0QdXA9OGW0PO86h4goDSW06Kg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tabs@11.7.3: + resolution: {integrity: sha512-5nd2NVss9TprPRV9r8N05SjQyAE7zDrLejxFLcbJ+BdLxSwnGnk3ws/Iq0smqKZUnPQC0XEvnpF3+zlllUUT2w==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tabs@9.7.0: + resolution: {integrity: sha512-kvmgp8/MfLzFZ06hWHignqomFQ5nF7BqKr5O1FfhE4VKsGrep52YSF/1MvS5oe0NPcI9XGNS2p751C5v6cYDpQ==} + peerDependencies: + react: '>=15.0.0' + + rc-textarea@0.3.7: + resolution: {integrity: sha512-yCdZ6binKmAQB13hc/oehh0E/QRwoPP1pjF21aHBxlgXO3RzPF6dUu4LG2R4FZ1zx/fQd2L1faktulrXOM/2rw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-time-picker@3.7.3: + resolution: {integrity: sha512-Lv1Mvzp9fRXhXEnRLO4nW6GLNxUkfAZ3RsiIBsWjGjXXvMNjdr4BX/ayElHAFK0DoJqOhm7c5tjmIYpEOwcUXg==} + + rc-tooltip@3.7.3: + resolution: {integrity: sha512-dE2ibukxxkrde7wH9W8ozHKUO4aQnPZ6qBHtrTH9LoO836PjDdiaWO73fgPB05VfJs9FbZdmGPVEbXCeOP99Ww==} + + rc-tooltip@5.1.1: + resolution: {integrity: sha512-alt8eGMJulio6+4/uDm7nvV+rJq9bsfxFDCI0ljPdbuoygUscbsMYb6EQgwib/uqsXQUvzk+S7A59uYHmEgmDA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tree-select@2.9.4: + resolution: {integrity: sha512-0HQkXAN4XbfBW20CZYh3G+V+VMrjX42XRtDCpyv6PDUm5vikC0Ob682ZBCVS97Ww2a5Hf6Ajmu0ahWEdIEpwhg==} + + rc-tree-select@4.3.3: + resolution: {integrity: sha512-0tilOHLJA6p+TNg4kD559XnDX3PTEYuoSF7m7ryzFLAYvdEEPtjn0QZc5z6L0sMKBiBlj8a2kf0auw8XyHU3lA==} + peerDependencies: + react: '*' + react-dom: '*' + + rc-tree@2.1.4: + resolution: {integrity: sha512-Xey794Iavgs8YldFlXcZLOhfcIhlX5Oz/yfKufknBXf2AlZCOkc7aHqSM9uTF7fBPtTGPhPxNEfOqHfY7b7xng==} + peerDependencies: + react: '*' + react-dom: '*' + + rc-tree@4.1.5: + resolution: {integrity: sha512-q2vjcmnBDylGZ9/ZW4F9oZMKMJdbFWC7um+DAQhZG1nqyg1iwoowbBggUDUaUOEryJP+08bpliEAYnzJXbI5xQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-trigger@2.6.5: + resolution: {integrity: sha512-m6Cts9hLeZWsTvWnuMm7oElhf+03GOjOLfTuU0QmdB9ZrW7jR2IpI5rpNM7i9MvAAlMAmTx5Zr7g3uu/aMvZAw==} + + rc-trigger@3.0.0: + resolution: {integrity: sha512-hQxbbJpo23E2QnYczfq3Ec5J5tVl2mUDhkqxrEsQAqk16HfADQg+iKNWzEYXyERSncdxfnzYuaBgy764mNRzTA==} + + rc-trigger@5.3.4: + resolution: {integrity: sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-upload@2.9.4: + resolution: {integrity: sha512-WXt0HGxXyzLrPV6iec/96Rbl/6dyrAW8pKuY6wwD7yFYwfU5bjgKjv7vC8KNMJ6wzitFrZjnoiogNL3dF9dj3Q==} + + rc-upload@4.3.6: + resolution: {integrity: sha512-Bt7ESeG5tT3IY82fZcP+s0tQU2xmo1W6P3S8NboUUliquJLQYLkUcsaExi3IlBVr43GQMCjo30RA2o0i70+NjA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-util@4.21.1: + resolution: {integrity: sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==} + + rc-util@5.44.4: + resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-virtual-list@3.19.2: + resolution: {integrity: sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-codemirror2@7.3.0: + resolution: {integrity: sha512-gCgJPXDX+5iaPolkHAu1YbJ92a2yL7Je4TuyO3QEqOtI/d6mbEk08l0oIm18R4ctuT/Sl87X63xIMBnRQBXYXA==} + peerDependencies: + codemirror: 5.x + react: '>=15.5 <=17.x' + + react-color@2.19.3: + resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} + peerDependencies: + react: '*' + + react-dom@16.14.0: + resolution: {integrity: sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==} + peerDependencies: + react: ^16.14.0 + + react-entry-template-loader@1.0.3: + resolution: {integrity: sha512-wMtt3DBI9i+zXP9vZ3pfYoWo/9J9En42l+kvMubqPBWcCOiVt3UKp9xniaRvkmvq7q1AOIQf3Yy42tb5PNuEFw==} + engines: {node: '>=6.0.0'} + + react-hot-loader@4.13.1: + resolution: {integrity: sha512-ZlqCfVRqDJmMXTulUGic4lN7Ic1SXgHAFw7y/Jb7t25GBgTR0fYAJ8uY4mrpxjRyWGWmqw77qJQGnYbzCvBU7g==} + engines: {node: '>= 6'} + peerDependencies: + '@types/react': ^15.0.0 || ^16.0.0 || ^17.0.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-lazy-load@3.1.14: + resolution: {integrity: sha512-7tsOItf2HmEwhEWMaA/a2XlShuya7rBxqWAR0TPMO1XSf6ybxSDI2bMV8M6vtWkveX9TlSpb0qLB7NMMpDHVDQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0-0 || ^16.0.0 || ^17.0.0 + react-dom: ^0.14.0 || ^15.0.0-0 || ^16.0.0 || ^17.0.0 + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-loadable@5.5.0: + resolution: {integrity: sha512-C8Aui0ZpMd4KokxRdVAm2bQtI03k2RMRNzOB+IipV3yxFTSVICv7WoUr5L9ALB5BmKO1iHgZtWM8EvYG83otdg==} + peerDependencies: + react: '*' + + react-markdown@6.0.3: + resolution: {integrity: sha512-kQbpWiMoBHnj9myLlmZG9T1JdoT/OEyHK7hqM6CqFT14MAkgWiWBUYijLyBmxbntaN6dCDicPcUhWhci1QYodg==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + react-redux@7.2.9: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-router-config@1.0.0-beta.4: + resolution: {integrity: sha512-4jWfQK+PNAYeMU7dZ5h/dNDTReVjm41p761+XP+II360Jz5TfzHiuP27tAasIQ+JcGWwX2l2dCWG8jEmcS5SBA==} + peerDependencies: + react: '>=15' + react-router: ^4.2.0 + + react-router-dom@4.3.1: + resolution: {integrity: sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==} + peerDependencies: + react: '>=15' + + react-router-redux@4.0.8: + resolution: {integrity: sha512-lzlK+S6jZnn17BZbzBe6F8ok3YAhGAUlyWgRu3cz5mT199gKxfem5lNu3qcgzRiVhNEOFVG0/pdT+1t4aWhoQw==} + + react-router@4.3.1: + resolution: {integrity: sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==} + peerDependencies: + react: '>=15' + + react-slick@0.25.2: + resolution: {integrity: sha512-8MNH/NFX/R7zF6W/w+FS5VXNyDusF+XDW1OU0SzODEU7wqYB+ZTGAiNJ++zVNAVqCAHdyCybScaUB+FCZOmBBw==} + peerDependencies: + react: ^0.14.0 || ^15.0.1 || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.1 || ^16.0.0 + + react-syntax-highlighter@15.6.6: + resolution: {integrity: sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==} + peerDependencies: + react: '>= 0.14.0' + + react@16.9.0: + resolution: {integrity: sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==} + engines: {node: '>=0.10.0'} + + reactcss@1.2.3: + resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} + peerDependencies: + react: '*' + + read-pkg-up@1.0.1: + resolution: {integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==} + engines: {node: '>=0.10.0'} + + read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + + read-pkg-up@4.0.0: + resolution: {integrity: sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==} + engines: {node: '>=6'} + + read-pkg-up@5.0.0: + resolution: {integrity: sha512-XBQjqOBtTzyol2CpsQOw8LHV0XbDZVG7xMMjmXAJomlVY03WOBRmYgDJETlvcg0H63AJvPRwT7GFi5rvOzUOKg==} + engines: {node: '>=8'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@1.1.0: + resolution: {integrity: sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==} + engines: {node: '>=0.10.0'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + readable-stream@1.1.14: + resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@2.2.1: + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + ready-callback@2.1.0: + resolution: {integrity: sha512-pyoQjeks8RvkzHbdDgSS1Faw+3xByvnWxccsIiBLOtFX+sp6pkpdSuIZJzfIgpzpOSOdVFVxrFEL+VcNL3+bBQ==} + engines: {node: '>=4.0.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + redent@1.0.0: + resolution: {integrity: sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==} + engines: {node: '>=0.10.0'} + + redent@2.0.0: + resolution: {integrity: sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==} + engines: {node: '>=4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redis-commands@1.7.0: + resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + redis@3.1.2: + resolution: {integrity: sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==} + engines: {node: '>=10'} + + redux-devtools-extension@2.13.9: + resolution: {integrity: sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==} + deprecated: Package moved to @redux-devtools/extension. + peerDependencies: + redux: ^3.1.0 || ^4.0.0 + + redux-thunk@2.4.2: + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + peerDependencies: + redux: ^4 + + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + refractor@3.6.0: + resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.10.5: + resolution: {integrity: sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==} + + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regenerator-transform@0.10.1: + resolution: {integrity: sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==} + + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpp@1.1.0: + resolution: {integrity: sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==} + engines: {node: '>=4.0.0'} + + regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + + regexpu-core@2.0.0: + resolution: {integrity: sha512-tJ9+S4oKjxY8IZ9jmjnp/mtytu1u3iyIQAfmI51IKWH6bFf7XR1ybtaO6j7INhZKXOTYADk7V5qxaqLkmNxiZQ==} + + registry-auth-token@3.4.0: + resolution: {integrity: sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==} + + registry-auth-token@4.2.2: + resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} + engines: {node: '>=6.0.0'} + + registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + + registry-url@5.1.0: + resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} + engines: {node: '>=8'} + + regjsgen@0.2.0: + resolution: {integrity: sha512-x+Y3yA24uF68m5GA+tBjbGYo64xXVJpbToBaWCoSNSc1hdk6dfctaRWrNFTVJZIIhL5GxW8zwjoixbnifnK59g==} + + regjsparser@0.1.5: + resolution: {integrity: sha512-jlQ9gYLfk2p3V5Ag5fYhA7fv7OHzd1KUH0PRP46xc3TgwjwgROIW572AfYg/X9kaNq/LJnu6oJcFRXlIrGoTRw==} + hasBin: true + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + remark-gfm@1.0.0: + resolution: {integrity: sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==} + + remark-parse@9.0.0: + resolution: {integrity: sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==} + + remark-rehype@8.1.0: + resolution: {integrity: sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + renderkid@2.0.7: + resolution: {integrity: sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==} + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + repeating@2.0.1: + resolution: {integrity: sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==} + engines: {node: '>=0.10.0'} + + request-promise-core@1.1.4: + resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} + engines: {node: '>=0.10.0'} + peerDependencies: + request: ^2.34 + + request-promise-native@1.0.9: + resolution: {integrity: sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==} + engines: {node: '>=0.12.0'} + deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + peerDependencies: + request: ^2.34 + + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@1.0.1: + resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + require-uncached@1.0.3: + resolution: {integrity: sha512-Xct+41K3twrbBHdxAgMoOS+cNcoqIjfM2/VxBF4LL2hVph7YsF8VSKyQ3BDFZwEVbok9yeDl2le/qo0S77WG2w==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-files@1.0.2: + resolution: {integrity: sha512-Q5cbKgzFCGB630+3BaK/vT2tNw5XkPXrO0hB/mozZmOP/tiL7xFJaJ9phds0hBzLxCh37Svr1NdUFtew0pMd+g==} + engines: {node: '>=4.0.0'} + + resolve-from@1.0.1: + resolution: {integrity: sha512-kT10v4dhrlLNcnO084hEjvXCI1wUG9qZLoz2RogxqDQQYy7IxjI/iMUkOtQTNEh6rzHxvdQWHsJyel1pKOVCxg==} + engines: {node: '>=0.10.0'} + + resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-global@1.0.0: + resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} + engines: {node: '>=8'} + + resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + + resolve-pkg@2.0.0: + resolution: {integrity: sha512-+1lzwXehGCXSeryaISr6WujZzowloigEofRB+dj75y9RRa/obVcYgbHJd53tdYw8pvZj8GojXaaENws8Ktw/hQ==} + engines: {node: '>=8'} + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + response-time@2.3.4: + resolution: {integrity: sha512-fiyq1RvW5/Br6iAtT8jN1XrNY8WPu2+yEypLbaijWry8WDZmn12azG9p/+c+qpEebURLlQmqCB8BNSu7ji+xQQ==} + engines: {node: '>= 0.8.0'} + + responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + retry-as-promised@2.3.2: + resolution: {integrity: sha512-KZMPON7wEhqU4pyWzXw/Ti8NYTVk5+qQ5OfAq3+L/3gJ2Fv+YaLVHbFSK80XlIfI9WrdP8c73bDTrh14SvTSKw==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rgb-regex@1.0.1: + resolution: {integrity: sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==} + + rgba-regex@1.0.0: + resolution: {integrity: sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==} + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + ripemd160@2.0.3: + resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} + engines: {node: '>= 0.8'} + + rmc-feedback@2.0.0: + resolution: {integrity: sha512-5PWOGOW7VXks/l3JzlOU9NIxRpuaSS8d9zA3UULUCuTKnpwBHNvv1jSJzxgbbCQeYzROWUpgKI4za3X4C/mKmQ==} + + rndm@1.2.0: + resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-node@1.0.0: + resolution: {integrity: sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==} + engines: {node: '>=4'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + run-queue@1.0.3: + resolution: {integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==} + + runscript@1.6.0: + resolution: {integrity: sha512-lI0ybcwtdC5Wz3aiVtMAK6U5jcTDeLseEBSXcz6ABtQeMmQGpj35dmzpmpy2C9Bn0k2wTjTRLZoya0NFt8Mxsg==} + engines: {node: '>=4.2.3'} + + rx-lite-aggregates@4.0.8: + resolution: {integrity: sha512-3xPNZGW93oCjiO7PtKxRK6iOVYBWBvtf9QHDfU23Oc+dLIQmAV//UnyXV/yihv81VS/UqoQPk4NegS8EFi55Hg==} + + rx-lite@4.0.8: + resolution: {integrity: sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + + safe-timers@1.1.0: + resolution: {integrity: sha512-9aqY+v5eMvmRaluUEtdRThV1EjlSElzO7HuCj0sTW9xvp++8iJ9t/RWGNWV6/WHcUJLHpyT2SNf/apoKTU2EpA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass-loader@10.2.0: + resolution: {integrity: sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 + sass: ^1.3.0 + webpack: ^4.36.0 || ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + + saxes@3.1.11: + resolution: {integrity: sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==} + engines: {node: '>=8'} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + sb-promise-queue@2.1.1: + resolution: {integrity: sha512-qXfdcJQMxMljxmPprn4Q4hl3pJmoljSCzUvvEBa9Kscewnv56n0KqrO6yWSrGLOL9E021wcGdPa39CHGKA6G0w==} + engines: {node: '>= 8'} + + sb-promisify@2.0.2: + resolution: {integrity: sha512-i7k8tMx+mJWIzM+Q5WWT7hfwUEaMfreDf0otZf+V41X3aKAjbLE9kCX4vR44BuqJalKHmGMYpWQP3yaMI2JP3g==} + + sb-scandir@2.0.0: + resolution: {integrity: sha512-SKbyMJB0DUt9OgN4tP2RBcn9OsR26DEpe+nwaDkQTNcrJSJI0FlLhXhBpTd/YEnlQ2GdLrbszSNekGLw4rweOQ==} + + sb-scandir@3.1.1: + resolution: {integrity: sha512-Q5xiQMtoragW9z8YsVYTAZcew+cRzdVBefPbb9theaIKw6cBo34WonP9qOCTKgyAmn/Ch5gmtAxT/krUgMILpA==} + engines: {node: '>= 8'} + + scheduler@0.19.1: + resolution: {integrity: sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==} + + scheduler@0.20.2: + resolution: {integrity: sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==} + + schema-utils@0.3.0: + resolution: {integrity: sha512-QaVYBaD9U8scJw2EBWnCBY+LJ0AD+/2edTaigDs0XLDLBfJmSUK9KGqktg1rb32U3z4j/XwvFwHHH1YfbYFd7Q==} + engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'} + + schema-utils@0.4.7: + resolution: {integrity: sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==} + engines: {node: '>= 4'} + + schema-utils@1.0.0: + resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==} + engines: {node: '>= 4'} + + schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + scmp@2.1.0: + resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} + deprecated: Just use Node.js's crypto.timingSafeEqual() + + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + + sdk-base@2.0.1: + resolution: {integrity: sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==} + + sdk-base@3.6.0: + resolution: {integrity: sha512-jxHUIrRLlAoRFRwiXKhOGjd6BeFWO/jz7tv+E7lbMSef6F9jzFN2Sv3hLW58oDDKscKaBGG6vQdkbXn7isE7fw==} + + sdk-base@4.2.1: + resolution: {integrity: sha512-B33iy/AkIpLyxexn/5v+8jmYHCyUaJizEQcmhvMY+SoiXViY1FEULbQibQH/EcOfzPwJqJ5UDeXyUJGO47Sq3g==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver-diff@2.1.0: + resolution: {integrity: sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==} + engines: {node: '>=0.10.0'} + + semver-diff@3.1.1: + resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} + engines: {node: '>=8'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + send@0.13.2: + resolution: {integrity: sha512-cQ0rmXHrdO2Iof08igV2bG/yXWD106ANwBg6DkGQNT2Vsznbgq6T0oAIQboy1GoFsIuy51jCim26aA9tj3Z3Zg==} + engines: {node: '>= 0.8.0'} + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + sendmessage@1.1.0: + resolution: {integrity: sha512-riI/U2etmtMKaVPe7zMnr++eG46F191F6Zycwrkm+/sEiHzucNXJETPJ5dQryNaDuHTpYdzmesEJQ2le1DxlMQ==} + engines: {node: '>= 0.10.0'} + + sentence-case@2.1.1: + resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + sequelize-cli@5.5.1: + resolution: {integrity: sha512-ZM4kUZvY3y14y+Rq3cYxGH7YDJz11jWHcN2p2x7rhAIemouu4CEXr5ebw30lzTBtyXV4j2kTO+nUjZOqzG7k+Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + sequelize@4.44.4: + resolution: {integrity: sha512-nkHmYkbwQK7uwpgW9VBalCBnQqQ8mslTdgcBthtJLORuPvAYRPlfkXZMVUU9TLLJt9CX+/y0MYg0DpcP6ywsEQ==} + engines: {node: '>=4.0.0'} + deprecated: 'Please update to v6 or higher! A migration guide can be found here: https://sequelize.org/v6/manual/upgrade-to-v6.html' + + serialize-javascript@1.9.1: + resolution: {integrity: sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==} + + serialize-javascript@2.1.2: + resolution: {integrity: sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==} + + serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + + serialize-json@1.0.3: + resolution: {integrity: sha512-TJvXOXSUEH4Lh2FNy1mYzNkUyBG7Ti5fRKGAbcpaDX3mLq23aT/5unC+cIFc5JTDi4/BHTaYLhynrboCCYrFaQ==} + engines: {node: '>= 4.0.0'} + + serve-favicon@2.3.2: + resolution: {integrity: sha512-oHEaA3ohvKxEWhjP97cQ6QuTTbMBF3AxDyMSvBtvnl1jXaB2Ik6kXE7nUtPM3YVU5VHCDe6n7JZrFCWzQuvXEQ==} + engines: {node: '>= 0.8.0'} + + serve-index@1.7.3: + resolution: {integrity: sha512-g18EQWY83uFBldFpCyK/a49yxQgIMEMLA6U9f66FiI848mLkMO8EY/xRAZAoCwNFwSUAiArCF3mdjaNXpd3ghw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.10.3: + resolution: {integrity: sha512-ScsFovjz3Db+vGgpofR/U8p8UULEcGV9akqyo8TQ1mMnjcxemE7Y5Muo+dvy3tJLY/doY2v1H61eCBMYGmwfrA==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + server-side-render-resource@1.2.0: + resolution: {integrity: sha512-QU2wZL4JJpbqQNa5fvLHq9rmbfqhHaQwt6SA2G1UJW2nwcb3f7Kk0vARNn7mGgBHIIAO3D8TUduLti+9KAHiHw==} + + service-worker-precache-webpack-plugin@1.3.5: + resolution: {integrity: sha512-UM8iL9bCgIEBG6b5SCmstWOZ0/nNHulGdev7PvX7rIIr2M8f7G+s02FTwqFs1nsq5tVrrfY87mv1bfoDrCf1FQ==} + + serviceworker-cache-polyfill@4.0.0: + resolution: {integrity: sha512-VMl1n99TbtKdO7DYNX0J9FQt1doo69V6fBniKC7o+CoJerbmFlQbsoxDa7P+b4b0tmpsdRIuzzS9sSJI7vFY2g==} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + + shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-escape@0.2.0: + resolution: {integrity: sha512-uRRBT2MfEOyxuECseCZd28jC1AJ8hmqqneWQ4VWUTgCAFvb3wKU1jLqj6egC4Exrr88ogg3dp+zroH4wJuaXzw==} + + shelljs@0.7.8: + resolution: {integrity: sha512-/YF5Uk8hcwi7ima04ppkbA4RaRMdPMBfwAvAf8sufYOxsJRtbdoBsT8vGvlb+799BrlGdYrd+oczIA2eN2JdWA==} + engines: {node: '>=0.11.0'} + hasBin: true + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + should-send-same-site-none@2.0.5: + resolution: {integrity: sha512-7dig49H7sKnv1v/GPoFQChGgJdEX9s2oy9TQBSD5RbUx7M9CCRjHMaFP06v+DZQNM0K+o8dBhvBAd4eEKirqbQ==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + slash@1.0.0: + resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==} + engines: {node: '>=0.10.0'} + + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slice-ansi@1.0.0: + resolution: {integrity: sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==} + engines: {node: '>=4'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + snake-case@2.1.0: + resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + + socket.io-adapter@1.1.2: + resolution: {integrity: sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==} + + socket.io-adapter@2.5.7: + resolution: {integrity: sha512-e0LyK91f3cUxTmv95/KzoLg47+zF+s/sbxRGDNsyG4dmIP8ZSX8ax6byOxfJXeNNtS/8AZlfD+uP7gBeR7DLlg==} + + socket.io-client@1.7.0: + resolution: {integrity: sha512-Z98fQi7AxSAPzzAFutU6ntZzUDTncPJ++knRtBK5FJFLefILuiGKAxD3rr7IwHpCGvdkm+OOFy8M4hI+yUObvA==} + + socket.io-client@2.5.0: + resolution: {integrity: sha512-lOO9clmdgssDykiOmVQQitwBAF3I6mYcQAo7hQ7AM6Ny5X7fp8hIJ3HcQs3Rjz4SoggoxA1OgrQyY8EgTbcPYw==} + + socket.io-parser@2.3.1: + resolution: {integrity: sha512-j6l4g/+yWQjmy1yByzg1DPFL4vxQw+NwCJatIxni/AE1wfm17FBtIKSWU4Ay+onrJwDxmC4eK4QS/04ZsqYwZQ==} + + socket.io-parser@3.3.5: + resolution: {integrity: sha512-pn+xG/oVnofxqteOawycpHw9QKclpNRa+Z7RW0vZmrIpkgZOVSVzBvY1YGR+p9kIEZXkeAjpHa21wRNLCZ6UAA==} + + socket.io-parser@3.4.4: + resolution: {integrity: sha512-9JWZRGFA1aFK5W9yPrHoaZBOuaPE4NO7VZr96uVAsP8zkAYZkzrKeQhNPKkiPJq3qQK5q9c5xKcMysE5PUnPbw==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + engines: {node: '>=10.0.0'} + + socket.io-redis@5.4.0: + resolution: {integrity: sha512-yCQm/Sywd3d08WXUfZRxt6O+JV2vWoPgWK6GVjiM0GkBtq5cpLOk8oILRPKbzTv1VEtSYmK41q0xzcgDinMbmQ==} + + socket.io@2.5.1: + resolution: {integrity: sha512-eaTE4tBKRD6RFoetquMbxgvcpvoDtRyIlkIMI/SMK2bsKvbENTsDeeu4GJ/z9c90yOWxB7b/eC+yKLPbHnH6bA==} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + + sort-keys@1.1.2: + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} + + sort-keys@2.0.0: + resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} + engines: {node: '>=4'} + + sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + + source-list-map@2.0.1: + resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-support@0.4.18: + resolution: {integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.1.43: + resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} + engines: {node: '>=0.8.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@1.1.5: + resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + speed-measure-webpack-plugin@1.6.0: + resolution: {integrity: sha512-hz09hUQeP74zHZOLtSJwpVsuLy8EWxuyGiMVZ2tuvIQcMlL5mGrmbe8RKvrVV5geRkxms9JusQuIBt0UnB0XFw==} + engines: {node: '>=6.0.0'} + peerDependencies: + webpack: ^1 || ^2 || ^3 || ^4 || ^5 + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + split2@2.2.0: + resolution: {integrity: sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==} + + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + ssh2-streams@0.4.10: + resolution: {integrity: sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==} + engines: {node: '>=5.2.0'} + + ssh2@0.8.9: + resolution: {integrity: sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==} + engines: {node: '>=5.2.0'} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + ssri@5.3.0: + resolution: {integrity: sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==} + + ssri@6.0.2: + resolution: {integrity: sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==} + + stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + standard-version@9.5.0: + resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} + engines: {node: '>=10'} + hasBin: true + + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + + stats-webpack-plugin@0.7.0: + resolution: {integrity: sha512-NT0YGhwuQ0EOX+uPhhUcI6/+1Sq/pMzNuSCBVT4GbFl/ac6I/JZefBcjlECNfAb1t3GOx5dEj1Z7x0cAxeeVLQ==} + peerDependencies: + webpack: '>=1.0.0' + + statuses@1.2.1: + resolution: {integrity: sha512-pVEuxHdSGrt8QmQ3LOZXLhSA6MP/iPqKzZeO6Squ7PNGkA/9MBsSfV0/L+bIxkoDmjF4tZcLpcVq/fkqoHvuKg==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + stealthy-require@1.1.1: + resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} + engines: {node: '>=0.10.0'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stream-browserify@2.0.2: + resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} + + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + + stream-counter@0.2.0: + resolution: {integrity: sha512-GjA2zKc2iXUUKRcOxXQmhEx0Ev3XHJ6c8yWGqhQjWwhGrqNwSsvq9YlRLgoGtZ5Kx2Ln94IedaqJ5GUG6aBbxA==} + engines: {node: '>=0.8.0'} + + stream-each@1.2.3: + resolution: {integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==} + + stream-http@2.8.3: + resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + + stream-slice@0.1.2: + resolution: {integrity: sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==} + + stream-wormhole@1.1.0: + resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} + engines: {node: '>=4.0.0'} + + streamifier@0.1.1: + resolution: {integrity: sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==} + engines: {node: '>=0.10'} + + streamsearch@0.1.2: + resolution: {integrity: sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==} + engines: {node: '>=0.8.0'} + + strict-uri-encode@1.1.0: + resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} + engines: {node: '>=0.10.0'} + + string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + + string-width@1.0.2: + resolution: {integrity: sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==} + engines: {node: '>=0.10.0'} + + string-width@2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + + string-width@3.1.0: + resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} + engines: {node: '>=6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringifier@1.4.1: + resolution: {integrity: sha512-7TGia2tzGIfw+Nki9r6kVxdP0vWeQ7oVZtyMnGxWsAJYe0XYV6VSGrfzUXm7r+icYfvpFlGNrwB+PYwFg+hfag==} + + stringify-package@1.0.1: + resolution: {integrity: sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==} + deprecated: This module is not used anymore, and has been replaced by @npmcli/package-json + + strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@2.0.0: + resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@1.0.1: + resolution: {integrity: sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==} + engines: {node: '>=0.10.0'} + hasBin: true + + strip-indent@2.0.0: + resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-loader@0.18.2: + resolution: {integrity: sha512-WPpJPZGUxWYHWIUMNNOYqql7zh85zGmr84FdTVWq52WTIkqlW9xSxD3QYWi/T31cqn9UNSsietVEgGn2aaSCzw==} + engines: {node: '>= 0.12.0'} + + style-search@0.1.0: + resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} + + style-to-object@0.3.0: + resolution: {integrity: sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==} + + stylehacks@4.0.3: + resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==} + engines: {node: '>=6.9.0'} + + stylelint-config-recommended@7.0.0: + resolution: {integrity: sha512-yGn84Bf/q41J4luis1AZ95gj0EQwRX8lWmGmBwkwBNSkpGSpl66XcPTulxGa/Z91aPoNGuIGBmFkcM1MejMo9Q==} + peerDependencies: + stylelint: ^14.4.0 + + stylelint-config-standard@25.0.0: + resolution: {integrity: sha512-21HnP3VSpaT1wFjFvv9VjvOGDtAviv47uTp3uFmzcN+3Lt+RYRv6oAplLaV51Kf792JSxJ6svCJh/G18E9VnCA==} + peerDependencies: + stylelint: ^14.4.0 + + stylelint-order@5.0.0: + resolution: {integrity: sha512-OWQ7pmicXufDw5BlRqzdz3fkGKJPgLyDwD1rFY3AIEfIH/LQY38Vu/85v8/up0I+VPiuGRwbc2Hg3zLAsJaiyw==} + peerDependencies: + stylelint: ^14.0.0 + + stylelint-scss@4.3.0: + resolution: {integrity: sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ==} + peerDependencies: + stylelint: ^14.5.1 + + stylelint@14.11.0: + resolution: {integrity: sha512-OTLjLPxpvGtojEfpESWM8Ir64Z01E89xsisaBMUP/ngOx1+4VG2DPRcUyCCiin9Rd3kPXPsh/uwHd9eqnvhsYA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + supports-color@2.0.0: + resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} + engines: {node: '>=0.8.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@6.0.0: + resolution: {integrity: sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==} + engines: {node: '>=6'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + svgo@1.3.2: + resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} + engines: {node: '>=4.0.0'} + deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. + hasBin: true + + sw-precache@5.2.1: + resolution: {integrity: sha512-8FAy+BP/FXE+ILfiVTt+GQJ6UEf4CVHD9OfhzH0JX+3zoy2uFk7Vn9EfXASOtVmmIVbL3jE/W8Z66VgPSZcMhw==} + engines: {node: '>=4.0.0'} + deprecated: 'Please migrate to Workbox: https://developers.google.com/web/tools/workbox/guides/migrations/migrate-from-sw' + hasBin: true + + sw-toolbox@3.6.0: + resolution: {integrity: sha512-v/hu7KQQtospyDLpZxz7m5c7s90aj53YEkJ/A8x3mLPlSgIkZ6RKJkTjBG75P1p/fo5IeSA4TycyJg3VSu/aPw==} + deprecated: 'Please migrate to Workbox: https://developers.google.com/web/tools/workbox/guides/migrations/migrate-from-sw' + + swap-case@1.1.2: + resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + table@4.0.2: + resolution: {integrity: sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tapable@0.2.9: + resolution: {integrity: sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==} + engines: {node: '>=0.6'} + + tapable@1.1.3: + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} + + tar-stream@1.6.2: + resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} + engines: {node: '>= 0.8.0'} + + tar@2.2.2: + resolution: {integrity: sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + tcp-base@3.2.0: + resolution: {integrity: sha512-fFAqH8QTbheuEbXLdbxTSe31Gkw6Lg3nq4loyrxIXM6+ILGdbYXEblgyuu7UltOkOHbP/q2iqaC+gIXXu0C5bg==} + engines: {node: '>= 6.0.0'} + + tcp-proxy.js@1.5.0: + resolution: {integrity: sha512-+BTS7RUwSHhkJdlqgCZ1Ta7U3Ei/vyaHe6uizj4t5IoUrFM55SLsFHzlZzOZvvtVdpGhQ0Y10UKjA7HTlxU5eQ==} + engines: {node: '>=14.0.0'} + + temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + + tempfile@3.0.0: + resolution: {integrity: sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==} + engines: {node: '>=8'} + + term-size@1.2.0: + resolution: {integrity: sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==} + engines: {node: '>=4'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + terraformer-wkt-parser@1.2.1: + resolution: {integrity: sha512-+CJyNLWb3lJ9RsZMTM66BY0MT3yIo4l4l22Jd9CrZuwzk54fsu4Sc7zejuS9fCITTuTQy3p06d4MZMVI7v5wSg==} + engines: {node: '>=4.2.6'} + deprecated: terraformer-wkt-parser is deprecated and no longer supported. Please use @terraformer/wkt. + + terraformer@1.0.12: + resolution: {integrity: sha512-MokUp0+MFal4CmJDVL6VAO1bKegeXcBM2RnPVfqcFIp2IIv8EbPAjG0j/vEy/vuKB8NVMMSF2vfpVS/QLe4DBg==} + engines: {node: '>=4.2.6'} + deprecated: terraformer is deprecated and no longer supported. Please use @terraformer/arcgis. + + terser-webpack-plugin@1.4.6: + resolution: {integrity: sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + + terser@4.8.1: + resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} + engines: {node: '>=6.0.0'} + hasBin: true + + test-exclude@5.2.3: + resolution: {integrity: sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==} + engines: {node: '>=6'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-loader@1.2.0: + resolution: {integrity: sha512-acJ0rvUk53+ly9cqYWNOpPqOgCkNpmHLPDGduNm4hDQWF7EDKEJXAopG9iEWsPPcml09wePkq3NF+ZUqnO6tbg==} + engines: {node: '>= 4.8 < 5.0.0 || >= 5.10'} + peerDependencies: + webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 + + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + timed-out@4.0.1: + resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==} + engines: {node: '>=0.10.0'} + + timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + + timers-ext@0.1.8: + resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} + engines: {node: '>=0.12'} + + timsort@0.3.0: + resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinydate@1.3.0: + resolution: {integrity: sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==} + engines: {node: '>=4'} + + title-case@2.1.1: + resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-array@0.1.4: + resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==} + + to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + + to-fast-properties@1.0.3: + resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==} + engines: {node: '>=0.10.0'} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + toposort-class@1.0.1: + resolution: {integrity: sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==} + + toposort@1.0.7: + resolution: {integrity: sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg==} + + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + + tough-cookie@3.0.1: + resolution: {integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + + traverse@0.6.11: + resolution: {integrity: sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==} + engines: {node: '>= 0.4'} + + trim-newlines@1.0.0: + resolution: {integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==} + engines: {node: '>=0.10.0'} + + trim-newlines@2.0.0: + resolution: {integrity: sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==} + engines: {node: '>=4'} + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + trim-right@1.0.1: + resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} + engines: {node: '>=0.10.0'} + + trough@1.0.5: + resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} + + tryer@1.0.1: + resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} + + ts-loader@8.4.0: + resolution: {integrity: sha512-6nFY3IZ2//mrPc+ImY3hNWx1vCHyEhl6V+wLmL4CZcm6g1CqX7UKrkc6y0i4FwcfOhxyMPCfaEvh20f4r9GNpw==} + engines: {node: '>=10.0.0'} + peerDependencies: + typescript: '*' + webpack: '*' + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + ts-node@7.0.1: + resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} + engines: {node: '>=4.2.0'} + hasBin: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsscmp@1.0.5: + resolution: {integrity: sha512-aP/vy9xYiYGvtpW4xBkxdoeqbT+nNeo/37cdQk3iSiGz0xKb20XwOgBSqYo1DzEqt1ycPubEfPU3oHgzsRRL3g==} + engines: {node: '>=0.6.x'} + + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + + tty-browserify@0.0.0: + resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + tweezer.js@1.5.0: + resolution: {integrity: sha512-aSiJz7rGWNAQq7hjMK9ZYDuEawXupcCWgl3woQQSoDP2Oh8O4srWb/uO1PzzHIsrPEOqrjJ2sUb9FERfzuBabQ==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + type-name@2.0.2: + resolution: {integrity: sha512-kkgkuqR/jKdKO5oh/I2SMu2dGbLXoJq0zkdgbxaqYK+hr9S9edwVVGf+tMUFTx2gH9TN2+Zu9JZ/Njonb3cjhA==} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typedarray.prototype.slice@1.0.5: + resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==} + engines: {node: '>= 0.4'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + + ua-parser-js@0.7.41: + resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} + hasBin: true + + uglify-es@3.3.9: + resolution: {integrity: sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==} + engines: {node: '>=0.8.0'} + deprecated: support for ECMAScript is superseded by `uglify-js` as of v3.13.0 + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uglify-js@3.4.10: + resolution: {integrity: sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==} + engines: {node: '>=0.8.0'} + hasBin: true + + uglifyjs-webpack-plugin@2.2.0: + resolution: {integrity: sha512-mHSkufBmBuJ+KHQhv5H0MXijtsoA1lynJt1lXOaotja8/I0pR4L9oGaPIZw+bQBOFittXZg9OC1sXSGO9D9ZYg==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + + uid-safe@2.0.0: + resolution: {integrity: sha512-PH/12q0a/sEGVS28fZ5evILW2Ayn13PwkYmCleDsIPm39vUIqN58hjyqtUd496kyMY6WkXtaDMDpS8nSCmNKTg==} + engines: {node: '>= 0.8'} + + uid-safe@2.1.4: + resolution: {integrity: sha512-MHTGzIDNPv1XhDK0MyKvEroobUhtpMa649/9SIFbTRO2dshLctD3zxOwQw+gQ+Mlp5osfMdUU1sjcO6Fw4rvCA==} + engines: {node: '>= 0.8'} + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + uid2@0.0.3: + resolution: {integrity: sha512-5gSP1liv10Gjp8cMEnFd6shzkL/D6W1uhXSFNCxDC+YI8+L8wkCYCbJ7n77Ezb4wE/xzMogecE+DtamEe9PZjg==} + + ultron@1.0.2: + resolution: {integrity: sha512-QMpnpVtYaWEeY+MwKDN/UdKlE/LsFZXM5lO1u7GaZzNgmIbGixHEmVMIKT+vqYOALu3m5GYQy9kz4Xu4IVn7Ow==} + + umzug@2.3.0: + resolution: {integrity: sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==} + engines: {node: '>=6.0.0'} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + unescape@1.0.1: + resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} + engines: {node: '>=0.10.0'} + + unified@9.2.2: + resolution: {integrity: sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==} + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + + uniqs@2.0.0: + resolution: {integrity: sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + unique-string@1.0.0: + resolution: {integrity: sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==} + engines: {node: '>=4'} + + unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + + unist-builder@2.0.3: + resolution: {integrity: sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==} + + unist-util-generated@1.1.6: + resolution: {integrity: sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-position@3.1.0: + resolution: {integrity: sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + universal-deep-strict-equal@1.2.2: + resolution: {integrity: sha512-UpnFi3/IF3jZHIHTdQXTHLCqpBP3805OFFRPHgvCS7k0oob2YVXxMTjS0U0g9qJTzqFRMwEnFFSlFLqt6zwjTQ==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unquote@1.1.1: + resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} + + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + unzip-response@2.0.1: + resolution: {integrity: sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==} + engines: {node: '>=4'} + + upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-notifier@2.5.0: + resolution: {integrity: sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==} + engines: {node: '>=4'} + + update-notifier@4.1.3: + resolution: {integrity: sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==} + engines: {node: '>=8'} + + upper-case-first@1.1.2: + resolution: {integrity: sha512-wINKYvI3Db8dtjikdAqoBbZoP6Q+PZUyfMR7pmwHzjC2quzSkUq5DmPrTtPEqHaz8AGtmsB4TqwapMTM1QAQOQ==} + + upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + url-loader@0.5.9: + resolution: {integrity: sha512-B7QYFyvv+fOBqBVeefsxv6koWWtjmHaMFT6KZWti4KRw8YUD/hOU+3AECvXuzyVawIBx3z7zQRejXCDSO5kk1Q==} + peerDependencies: + file-loader: '*' + + url-loader@1.1.2: + resolution: {integrity: sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^3.0.0 || ^4.0.0 + + url-parse-lax@1.0.0: + resolution: {integrity: sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==} + engines: {node: '>=0.10.0'} + + url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + urllib@2.44.0: + resolution: {integrity: sha512-zRCJqdfYllRDA9bXUtx+vccyRqtJPKsw85f44zH7zPD28PIvjMqIgw9VwoTLV7xTBWZsbebUFVHU5ghQcWku2A==} + engines: {node: '>= 0.10.0'} + peerDependencies: + proxy-agent: ^5.0.0 + peerDependenciesMeta: + proxy-agent: + optional: true + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + + utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util.promisify@1.0.0: + resolution: {integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==} + + util.promisify@1.0.1: + resolution: {integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==} + + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + + util@0.11.1: + resolution: {integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utility@1.18.0: + resolution: {integrity: sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==} + engines: {node: '>= 0.12.0'} + + utility@2.5.0: + resolution: {integrity: sha512-lDbOVde5UAKgtxrSyZNhqrTA7f7anba6DTqbsDWgUFk6PZlmr7djqPYw0FnL5a6TbJvRt38VmYqt07zVLzXG2A==} + engines: {node: '>= 16.0.0'} + + utils-merge@1.0.0: + resolution: {integrity: sha512-HwU9SLQEtyo+0uoKXd1nkLqigUWLB+QuNQR4OcmB73eWqksM5ovuqcycks2x043W8XVb75rG1HQ0h93TMXkzQQ==} + engines: {node: '>= 0.4.0'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validator@10.11.0: + resolution: {integrity: sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==} + engines: {node: '>= 0.10'} + + value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + + vary@1.0.1: + resolution: {integrity: sha512-yNsH+tC0r8quK2tg/yqkXqqaYzeKTkSqQ+8T6xCoWgOi/bU/omMYz+6k+I91JJJDeltJzI7oridTOq6OYkY0Tw==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vconsole-webpack-plugin@1.8.0: + resolution: {integrity: sha512-rqRPWZ+reNLxysT+xJSpwp+HIWB/WgMRq7xe/hjEb6lY/LY1fmqfGvejnZPHhIoF53zjl8iQLFk7/zQuGi1XVA==} + + vconsole@3.15.1: + resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} + + vendors@1.0.4: + resolution: {integrity: sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + + vfile@4.2.1: + resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} + + vhost@3.0.2: + resolution: {integrity: sha512-S3pJdWrpFWrKMboRU4dLYgMrTgoPALsmYwOvyebK2M6X95b9kQrjZy5rwl3uzzpfpENe/XrNYu/2U+e7/bmT5g==} + engines: {node: '>= 0.8.0'} + + vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + + vue-cli-plugin-commitlint@1.0.12: + resolution: {integrity: sha512-ztCZKCt12aI8WyiCAFlERQicxph5yazWlAw11SaQLE+C5PMhUDqrWSzDzRRpGvoOo1QfQy/OK/Ml6TzOYrk41A==} + + w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + + w3c-xmlserializer@1.1.2: + resolution: {integrity: sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==} + + w3c-xmlserializer@2.0.0: + resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} + engines: {node: '>=10'} + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + watchpack-chokidar2@2.0.1: + resolution: {integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==} + + watchpack@1.7.5: + resolution: {integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + + webpack-asset-file-plugin@1.0.2: + resolution: {integrity: sha512-Z358i5Us5KIQgBJeM5AGb+fTX/hzv0YkStmGOhaB9D06Vogl6kqdzvWEuaTdQGjy5wBrC6knynLmJH4UlptqOg==} + engines: {node: '>=6.0.0'} + + webpack-bundle-analyzer@3.9.0: + resolution: {integrity: sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==} + engines: {node: '>= 6.14.4'} + hasBin: true + + webpack-dev-middleware@3.6.0: + resolution: {integrity: sha512-oeXA3m+5gbYbDBGo4SvKpAHJJEGMoekUbHgo1RK7CP1sz7/WOSeu/dWJtSTk+rzDCLkPwQhGocgIq6lQqOyOwg==} + engines: {node: '>= 6'} + peerDependencies: + webpack: ^4.0.0 + + webpack-filter-warnings-plugin@1.2.1: + resolution: {integrity: sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==} + engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'} + peerDependencies: + webpack: ^2.0.0 || ^3.0.0 || ^4.0.0 + + webpack-hot-middleware@2.26.1: + resolution: {integrity: sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==} + + webpack-log@2.0.0: + resolution: {integrity: sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==} + engines: {node: '>= 6'} + + webpack-manifest-resource-plugin@4.2.7: + resolution: {integrity: sha512-qeOJuSnm5yb2T0rIsq7F9WYygxbwyDGWagDtivu4ZHrw2jsCjx9D7Q/ZYi/NaOyGcru/1u3zmP46uIpL67yF3w==} + engines: {node: '>=8.0.0'} + + webpack-merge@4.2.2: + resolution: {integrity: sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==} + + webpack-node-externals@1.7.2: + resolution: {integrity: sha512-ajerHZ+BJKeCLviLUUmnyd5B4RavLF76uv3cs6KNuO8W+HuQaEs0y0L7o40NQxdPy5w0pcv8Ew7yPUAQG0UdCg==} + + webpack-sources@1.4.3: + resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} + + webpack-tool@4.5.4: + resolution: {integrity: sha512-PB09sdhicz2291If3dGSRv0fqpn4toORZFtVJLarAqJKKrqnzJ+3WbPIhKG4b1E5e6sGN6OYq+aHzrXLnK5TVg==} + engines: {node: '>=6.0.0'} + + webpack@4.28.4: + resolution: {integrity: sha512-NxjD61WsK/a3JIdwWjtIpimmvE6UrRi3yG54/74Hk9rwNj5FPkA4DJCf1z4ByDWLkvZhTZE+P3C/eh6UD5lDcw==} + engines: {node: '>=6.11.5'} + hasBin: true + peerDependencies: + webpack-cli: '*' + webpack-command: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + webpack-command: + optional: true + + webpack@4.47.0: + resolution: {integrity: sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==} + engines: {node: '>=6.11.5'} + hasBin: true + peerDependencies: + webpack-cli: '*' + webpack-command: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + webpack-command: + optional: true + + whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@1.0.0: + resolution: {integrity: sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wide-align@1.1.3: + resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==} + + widest-line@2.0.1: + resolution: {integrity: sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==} + engines: {node: '>=4'} + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + win-release@1.1.1: + resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==} + engines: {node: '>=0.10.0'} + + wkx@0.4.8: + resolution: {integrity: sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + worker-farm@1.7.0: + resolution: {integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==} + + wrap-ansi@2.1.0: + resolution: {integrity: sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==} + engines: {node: '>=0.10.0'} + + wrap-ansi@5.1.0: + resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} + engines: {node: '>=6'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + write-json-file@2.3.0: + resolution: {integrity: sha512-84+F0igFp2dPD6UpAQjOUX3CdKUOqUzn6oE9sDBNzUXINR5VceJ1rauZltqQB/bcYsx3EpKys4C7/PivKUAiWQ==} + engines: {node: '>=4'} + + write-json-file@3.2.0: + resolution: {integrity: sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ==} + engines: {node: '>=6'} + + write-pkg@3.2.0: + resolution: {integrity: sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==} + engines: {node: '>=4'} + + write@0.2.1: + resolution: {integrity: sha512-CJ17OoULEKXpA5pef3qLj5AxTJ6mSt7g84he2WIskKwqFO4T97d5V7Tadl0DYDk7qyUOQD5WlUlOMChaYrhxeA==} + engines: {node: '>=0.10.0'} + + write@1.0.3: + resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==} + engines: {node: '>=4'} + + ws@1.1.1: + resolution: {integrity: sha512-TRtCup+Fxoy1sW9funE4kPxA0KfaMc7g68DoKN+Uu9Ej+zr9We3DWVJ2XgiGtXlibqA7IWOV+Xe6xlUxPputfg==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@6.2.3: + resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wt@1.2.0: + resolution: {integrity: sha512-nJttxFyxnIvWBWYcy7Px4XkXbRoEe77by66d0FDof266Mv00vVBNcUGa00rL1+0DdsXG75LG1rwBnBfUa2bNLA==} + engines: {node: '>= 0.12.9'} + + wtf-8@1.0.0: + resolution: {integrity: sha512-qfR6ovmRRMxNHgUNYI9LRdVofApe/eYrv4ggNOvvCP+pPdEo9Ym93QN4jUceGD6PignBbp2zAzgoE7GibAdq2A==} + + xdg-basedir@3.0.0: + resolution: {integrity: sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==} + engines: {node: '>=4'} + + xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + + xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xmlhttprequest-ssl@1.5.3: + resolution: {integrity: sha512-kauAa/1btT613pYX92WXR6kx5trYeckB5YMd3pPvtkMo2Twdfhwl683M8NiSqWHHo97xAC6bnvK1DWFKxfmejg==} + engines: {node: '>=0.4.0'} + + xmlhttprequest-ssl@1.6.3: + resolution: {integrity: sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==} + engines: {node: '>=0.4.0'} + + xss@1.0.15: + resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} + engines: {node: '>= 0.10.0'} + hasBin: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + xterm-addon-attach@0.6.0: + resolution: {integrity: sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg==} + deprecated: This package is now deprecated. Move to @xterm/addon-attach instead. + peerDependencies: + xterm: ^4.0.0 + + xterm-addon-fit@0.5.0: + resolution: {integrity: sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==} + deprecated: This package is now deprecated. Move to @xterm/addon-fit instead. + peerDependencies: + xterm: ^4.0.0 + + xterm@4.19.0: + resolution: {integrity: sha512-c3Cp4eOVsYY5Q839dR5IejghRPpxciGmLWWaP9g+ppfMeBChMeLa1DCA+pmX/jyDZ+zxFOmlJL/82qVdayVoGQ==} + deprecated: This package is now deprecated. Move to @xterm/xterm instead. + + y18n@3.2.2: + resolution: {integrity: sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yargonaut@1.1.4: + resolution: {integrity: sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==} + + yargs-parser@10.1.0: + resolution: {integrity: sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==} + + yargs-parser@13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@5.0.1: + resolution: {integrity: sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==} + + yargs-unparser@1.6.0: + resolution: {integrity: sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==} + engines: {node: '>=6'} + + yargs@13.3.2: + resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@7.1.2: + resolution: {integrity: sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==} + + yazl@2.5.1: + resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} + + yeast@0.1.2: + resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==} + + ylru@1.4.0: + resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} + engines: {node: '>= 4.0.0'} + + ylru@2.0.0: + resolution: {integrity: sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==} + engines: {node: '>= 18.19.0'} + + yn@2.0.0: + resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} + engines: {node: '>=4'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + ypkgfiles@1.6.0: + resolution: {integrity: sha512-q8vgLzZy5CO1LUBFPWOkYpqCkAEaWdXTAAIfLREB72vxnXd+vUZvU3Qxb694TyPc56zA3t8fZIcBNj8fWtSR2A==} + engines: {node: '>=4.0.0'} + hasBin: true + + zip-stream@1.2.0: + resolution: {integrity: sha512-2olrDUuPM4NvRIgGPhvrp84f7/HmWR6RiQrgwFF2VctmnssFiogtYL3DcA8Vl2bsSmju79sVXe38TsII7JleUg==} + engines: {node: '>= 0.10.0'} + + zlogger@1.1.0: + resolution: {integrity: sha512-WjRPkjHe4rajXun45zZRAnViTSLkIr2PEFI303u+3NYIkhWqxhpTr0j67XPzhTJMmDH27pGoTSxGg/qP8FiaJA==} + engines: {node: '>=4.3.1'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zwitch@1.0.5: + resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} + +snapshots: + + '@ant-design/colors@3.2.2': + dependencies: + tinycolor2: 1.6.0 + + '@ant-design/colors@6.0.0': + dependencies: + '@ctrl/tinycolor': 3.6.1 + + '@ant-design/create-react-context@0.2.6(prop-types@15.8.1)(react@16.9.0)': + dependencies: + gud: 1.0.0 + prop-types: 15.8.1 + react: 16.9.0 + warning: 4.0.3 + + '@ant-design/css-animation@1.7.3': {} + + '@ant-design/icons-react@2.0.1(@ant-design/icons@2.1.1)(react@16.9.0)': + dependencies: + '@ant-design/colors': 3.2.2 + '@ant-design/icons': 2.1.1 + babel-runtime: 6.26.0 + react: 16.9.0 + + '@ant-design/icons-svg@4.4.2': {} + + '@ant-design/icons@2.1.1': {} + + '@ant-design/icons@4.5.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0)': + dependencies: + '@ant-design/colors': 6.0.0 + '@ant-design/icons-svg': 4.4.2 + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + insert-css: 2.0.0 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + transitivePeerDependencies: + - react-dom + + '@ant-design/icons@4.8.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0)': + dependencies: + '@ant-design/colors': 6.0.0 + '@ant-design/icons-svg': 4.4.2 + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + lodash: 4.18.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + '@ant-design/react-slick@0.28.4(react@16.9.0)': + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + json2mq: 0.2.0 + lodash: 4.18.1 + react: 16.9.0 + resize-observer-polyfill: 1.5.1 + + '@babel/code-frame@7.0.0-beta.44': + dependencies: + '@babel/highlight': 7.0.0-beta.44 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.0.0-beta.44': + dependencies: + '@babel/types': 7.0.0-beta.44 + jsesc: 2.5.2 + lodash: 4.18.1 + source-map: 0.5.7 + trim-right: 1.0.1 + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-function-name@7.0.0-beta.44': + dependencies: + '@babel/helper-get-function-arity': 7.0.0-beta.44 + '@babel/template': 7.0.0-beta.44 + '@babel/types': 7.0.0-beta.44 + + '@babel/helper-get-function-arity@7.0.0-beta.44': + dependencies: + '@babel/types': 7.0.0-beta.44 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-split-export-declaration@7.0.0-beta.44': + dependencies: + '@babel/types': 7.0.0-beta.44 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/highlight@7.0.0-beta.44': + dependencies: + chalk: 2.4.2 + esutils: 2.0.3 + js-tokens: 3.0.2 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/polyfill@7.12.1': + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.13.11 + + '@babel/runtime-corejs3@7.29.2': + dependencies: + core-js-pure: 3.49.0 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.0.0-beta.44': + dependencies: + '@babel/code-frame': 7.0.0-beta.44 + '@babel/types': 7.0.0-beta.44 + babylon: 7.0.0-beta.44 + lodash: 4.18.1 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.0.0-beta.44': + dependencies: + '@babel/code-frame': 7.0.0-beta.44 + '@babel/generator': 7.0.0-beta.44 + '@babel/helper-function-name': 7.0.0-beta.44 + '@babel/helper-split-export-declaration': 7.0.0-beta.44 + '@babel/types': 7.0.0-beta.44 + babylon: 7.0.0-beta.44 + debug: 3.2.7 + globals: 11.12.0 + invariant: 2.2.4 + lodash: 4.18.1 + transitivePeerDependencies: + - supports-color + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.0.0-beta.44': + dependencies: + esutils: 2.0.3 + lodash: 4.18.1 + to-fast-properties: 2.0.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@commitlint/cli@8.3.6': + dependencies: + '@commitlint/format': 8.3.6 + '@commitlint/lint': 8.3.6 + '@commitlint/load': 8.3.6 + '@commitlint/read': 8.3.6 + babel-polyfill: 6.26.0 + chalk: 2.4.2 + get-stdin: 7.0.0 + lodash: 4.17.21 + meow: 5.0.0 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + + '@commitlint/config-conventional@8.3.6': + dependencies: + conventional-changelog-conventionalcommits: 4.2.1 + + '@commitlint/config-validator@21.0.1': + dependencies: + '@commitlint/types': 21.0.1 + ajv: 8.20.0 + optional: true + + '@commitlint/ensure@8.3.6': + dependencies: + lodash: 4.17.21 + + '@commitlint/execute-rule@21.0.1': + optional: true + + '@commitlint/execute-rule@8.3.6': {} + + '@commitlint/format@8.3.6': + dependencies: + chalk: 2.4.2 + + '@commitlint/is-ignored@8.3.6': + dependencies: + semver: 6.3.0 + + '@commitlint/lint@8.3.6': + dependencies: + '@commitlint/is-ignored': 8.3.6 + '@commitlint/parse': 8.3.6 + '@commitlint/rules': 8.3.6 + babel-runtime: 6.26.0 + lodash: 4.17.21 + + '@commitlint/load@21.0.1(@types/node@25.9.1)(typescript@4.7.4)': + dependencies: + '@commitlint/config-validator': 21.0.1 + '@commitlint/execute-rule': 21.0.1 + '@commitlint/resolve-extends': 21.0.1 + '@commitlint/types': 21.0.1 + cosmiconfig: 9.0.1(typescript@4.7.4) + cosmiconfig-typescript-loader: 6.3.0(@types/node@25.9.1)(cosmiconfig@9.0.1(typescript@4.7.4))(typescript@4.7.4) + es-toolkit: 1.46.1 + is-plain-obj: 4.1.0 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + optional: true + + '@commitlint/load@8.3.6': + dependencies: + '@commitlint/execute-rule': 8.3.6 + '@commitlint/resolve-extends': 8.3.6 + babel-runtime: 6.26.0 + chalk: 2.4.2 + cosmiconfig: 5.2.1 + lodash: 4.17.21 + resolve-from: 5.0.0 + + '@commitlint/message@8.3.6': {} + + '@commitlint/parse@8.3.6': + dependencies: + conventional-changelog-angular: 1.6.6 + conventional-commits-parser: 3.2.4 + lodash: 4.18.1 + + '@commitlint/read@8.3.6': + dependencies: + '@commitlint/top-level': 8.3.6 + '@marionebl/sander': 0.6.1 + babel-runtime: 6.26.0 + git-raw-commits: 2.0.11 + + '@commitlint/resolve-extends@21.0.1': + dependencies: + '@commitlint/config-validator': 21.0.1 + '@commitlint/types': 21.0.1 + es-toolkit: 1.46.1 + global-directory: 5.0.0 + resolve-from: 5.0.0 + optional: true + + '@commitlint/resolve-extends@8.3.6': + dependencies: + import-fresh: 3.3.1 + lodash: 4.17.21 + resolve-from: 5.0.0 + resolve-global: 1.0.0 + + '@commitlint/rules@8.3.6': + dependencies: + '@commitlint/ensure': 8.3.6 + '@commitlint/message': 8.3.6 + '@commitlint/to-lines': 8.3.6 + babel-runtime: 6.26.0 + + '@commitlint/to-lines@8.3.6': {} + + '@commitlint/top-level@8.3.6': + dependencies: + find-up: 4.1.0 + + '@commitlint/types@21.0.1': + dependencies: + conventional-commits-parser: 6.4.0 + picocolors: 1.1.1 + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.1.2)': + dependencies: + postcss-selector-parser: 6.1.2 + + '@ctrl/tinycolor@3.6.1': {} + + '@easy-team/koa-history-api-fallback@1.0.0': {} + + '@eggjs/router@2.2.0': + dependencies: + co: 4.6.0 + http-errors: 1.8.1 + inflection: 1.13.4 + koa-compose: 3.2.1 + koa-convert: 1.2.0 + methods: 1.1.2 + path-to-regexp: 1.9.0 + urijs: 1.19.11 + utility: 1.18.0 + + '@eggjs/yauzl@2.11.0': + dependencies: + buffer-crc32: 0.2.13 + fd-slicer2: 1.2.0 + + '@eslint-community/eslint-utils@4.9.1(eslint@8.22.0)': + dependencies: + eslint: 8.22.0 + eslint-visitor-keys: 3.4.3 + + '@eslint/eslintrc@1.4.1': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@hapi/bourne@3.0.0': {} + + '@hono/node-server@1.19.14(hono@4.12.21)': + dependencies: + hono: 4.12.21 + + '@hot-loader/react-dom@16.14.0(react@16.9.0)': + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 16.9.0 + scheduler: 0.19.1 + + '@hot-loader/react-dom@17.0.2(react@16.9.0)': + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react: 16.9.0 + scheduler: 0.20.2 + + '@humanwhocodes/config-array@0.10.7': + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/gitignore-to-minimatch@1.0.2': {} + + '@humanwhocodes/object-schema@1.2.1': {} + + '@hutson/parse-repository-url@3.0.2': {} + + '@icons/material@0.2.4(react@16.9.0)': + dependencies: + react: 16.9.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/types@25.5.0': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 1.1.2 + '@types/yargs': 15.0.20 + chalk: 3.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@koa/cors@3.4.3': + dependencies: + vary: 1.1.2 + + '@marionebl/sander@0.6.1': + dependencies: + graceful-fs: 4.2.11 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.21) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.21 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@mrmlnc/readdir-enhanced@2.2.1': + dependencies: + call-me-maybe: 1.0.2 + glob-to-regexp: 0.3.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@1.1.3': {} + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@one-ini/wasm@0.1.1': {} + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@simple-libs/stream-utils@1.2.0': + optional: true + + '@sindresorhus/is@0.14.0': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@szmarczak/http-timer@1.1.2': + dependencies: + defer-to-connect: 1.1.3 + + '@tootallnate/once@1.1.2': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/accepts@1.3.7': + dependencies: + '@types/node': 25.9.1 + + '@types/bluebird@3.5.42': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.9.1 + + '@types/classnames@2.3.4': + dependencies: + classnames: 2.5.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.9.1 + + '@types/content-disposition@0.5.9': {} + + '@types/continuation-local-storage@3.2.7': + dependencies: + '@types/node': 25.9.1 + + '@types/cookies@0.9.2': + dependencies: + '@types/connect': 3.4.38 + '@types/express': 5.0.6 + '@types/keygrip': 1.0.6 + '@types/node': 25.9.1 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 25.9.1 + + '@types/dargs@5.1.0': {} + + '@types/depd@1.1.37': + dependencies: + '@types/node': 25.9.1 + + '@types/eslint-visitor-keys@1.0.0': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.9.1 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/file-saver@2.0.7': {} + + '@types/geojson@1.0.6': {} + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 6.0.0 + '@types/node': 25.9.1 + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/history@4.7.11': {} + + '@types/hoist-non-react-statics@3.3.7(@types/react@16.14.70)': + dependencies: + '@types/react': 16.14.70 + hoist-non-react-statics: 3.3.2 + + '@types/html2canvas@0.0.36': {} + + '@types/http-assert@1.5.6': {} + + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.17': + dependencies: + '@types/node': 25.9.1 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@1.1.2': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-lib-report': 3.0.3 + + '@types/js-cookie@2.2.7': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/keygrip@1.0.6': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 25.9.1 + + '@types/koa-compose@3.2.9': + dependencies: + '@types/koa': 2.15.2 + + '@types/koa-router@7.4.9': + dependencies: + '@types/koa': 2.15.2 + + '@types/koa@2.15.2': + dependencies: + '@types/accepts': 1.3.7 + '@types/content-disposition': 0.5.9 + '@types/cookies': 0.9.2 + '@types/http-assert': 1.5.6 + '@types/http-errors': 2.0.5 + '@types/keygrip': 1.0.6 + '@types/koa-compose': 3.2.9 + '@types/node': 25.9.1 + + '@types/lodash@4.17.24': {} + + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + + '@types/minimatch@6.0.0': + dependencies: + minimatch: 3.1.5 + + '@types/minimist@1.2.5': {} + + '@types/node-ssh@7.0.6': + dependencies: + '@types/node': 25.9.1 + '@types/ssh2': 1.15.5 + '@types/ssh2-streams': 0.1.13 + + '@types/node@10.17.60': {} + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + + '@types/normalize-package-data@2.4.4': {} + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/q@1.5.8': {} + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-color@3.0.13(@types/react@16.14.70)': + dependencies: + '@types/react': 16.14.70 + '@types/reactcss': 1.2.13(@types/react@16.14.70) + + '@types/react-dom@16.9.25(@types/react@16.14.70)': + dependencies: + '@types/react': 16.14.70 + + '@types/react-redux@7.1.34': + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@16.14.70) + '@types/react': 16.14.70 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + + '@types/react-router-config@5.0.11': + dependencies: + '@types/history': 4.7.11 + '@types/react': 16.14.70 + '@types/react-router': 5.1.20 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 16.14.70 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 16.14.70 + + '@types/react-slick@0.23.13': + dependencies: + '@types/react': 16.14.70 + + '@types/react@16.14.70': + dependencies: + '@types/prop-types': 15.7.15 + '@types/scheduler': 0.16.8 + csstype: 3.2.3 + + '@types/reactcss@1.2.13(@types/react@16.14.70)': + dependencies: + '@types/react': 16.14.70 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 25.9.1 + + '@types/scheduler@0.16.8': {} + + '@types/semver@7.7.1': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 25.9.1 + + '@types/sequelize@4.28.20': + dependencies: + '@types/bluebird': 3.5.42 + '@types/continuation-local-storage': 3.2.7 + '@types/lodash': 4.17.24 + '@types/validator': 13.15.10 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.9.1 + + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 25.9.1 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + + '@types/unist@2.0.11': {} + + '@types/validator@13.15.10': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.1 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@12.0.20': {} + + '@types/yargs@15.0.20': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@2.34.0(@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4))(eslint@4.19.1)(typescript@4.7.4)': + dependencies: + '@typescript-eslint/experimental-utils': 2.34.0(eslint@4.19.1)(typescript@4.7.4) + '@typescript-eslint/parser': 2.34.0(eslint@4.19.1)(typescript@4.7.4) + eslint: 4.19.1 + functional-red-black-tree: 1.0.1 + regexpp: 3.2.0 + tsutils: 3.21.0(typescript@4.7.4) + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@5.30.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4)': + dependencies: + '@typescript-eslint/parser': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + '@typescript-eslint/scope-manager': 5.30.0 + '@typescript-eslint/type-utils': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + '@typescript-eslint/utils': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + debug: 4.4.3 + eslint: 8.22.0 + functional-red-black-tree: 1.0.1 + ignore: 5.3.2 + regexpp: 3.2.0 + semver: 7.8.0 + tsutils: 3.21.0(typescript@4.7.4) + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/experimental-utils@2.34.0(eslint@4.19.1)(typescript@4.7.4)': + dependencies: + '@types/json-schema': 7.0.15 + '@typescript-eslint/typescript-estree': 2.34.0(typescript@4.7.4) + eslint: 4.19.1 + eslint-scope: 5.1.1 + eslint-utils: 2.1.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4)': + dependencies: + '@types/eslint-visitor-keys': 1.0.0 + '@typescript-eslint/experimental-utils': 2.34.0(eslint@4.19.1)(typescript@4.7.4) + '@typescript-eslint/typescript-estree': 2.34.0(typescript@4.7.4) + eslint: 4.19.1 + eslint-visitor-keys: 1.3.0 + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4)': + dependencies: + '@typescript-eslint/scope-manager': 5.30.0 + '@typescript-eslint/types': 5.30.0 + '@typescript-eslint/typescript-estree': 5.30.0(typescript@4.7.4) + debug: 4.4.3 + eslint: 8.22.0 + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.30.0': + dependencies: + '@typescript-eslint/types': 5.30.0 + '@typescript-eslint/visitor-keys': 5.30.0 + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/type-utils@5.30.0(eslint@8.22.0)(typescript@4.7.4)': + dependencies: + '@typescript-eslint/utils': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + debug: 4.4.3 + eslint: 8.22.0 + tsutils: 3.21.0(typescript@4.7.4) + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.30.0': {} + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/typescript-estree@2.34.0(typescript@4.7.4)': + dependencies: + debug: 4.4.3 + eslint-visitor-keys: 1.3.0 + glob: 7.2.3 + is-glob: 4.0.3 + lodash: 4.18.1 + semver: 7.8.0 + tsutils: 3.21.0(typescript@4.7.4) + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@5.30.0(typescript@4.7.4)': + dependencies: + '@typescript-eslint/types': 5.30.0 + '@typescript-eslint/visitor-keys': 5.30.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.8.0 + tsutils: 3.21.0(typescript@4.7.4) + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@5.62.0(typescript@4.7.4)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.3 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.8.0 + tsutils: 3.21.0(typescript@4.7.4) + optionalDependencies: + typescript: 4.7.4 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.30.0(eslint@8.22.0)(typescript@4.7.4)': + dependencies: + '@types/json-schema': 7.0.15 + '@typescript-eslint/scope-manager': 5.30.0 + '@typescript-eslint/types': 5.30.0 + '@typescript-eslint/typescript-estree': 5.30.0(typescript@4.7.4) + eslint: 8.22.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0(eslint@8.22.0) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@5.62.0(eslint@8.22.0)(typescript@4.7.4)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.22.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.1 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.7.4) + eslint: 8.22.0 + eslint-scope: 5.1.1 + semver: 7.8.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@5.30.0': + dependencies: + '@typescript-eslint/types': 5.30.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@webassemblyjs/ast@1.7.11': + dependencies: + '@webassemblyjs/helper-module-context': 1.7.11 + '@webassemblyjs/helper-wasm-bytecode': 1.7.11 + '@webassemblyjs/wast-parser': 1.7.11 + + '@webassemblyjs/ast@1.9.0': + dependencies: + '@webassemblyjs/helper-module-context': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/wast-parser': 1.9.0 + + '@webassemblyjs/floating-point-hex-parser@1.7.11': {} + + '@webassemblyjs/floating-point-hex-parser@1.9.0': {} + + '@webassemblyjs/helper-api-error@1.7.11': {} + + '@webassemblyjs/helper-api-error@1.9.0': {} + + '@webassemblyjs/helper-buffer@1.7.11': {} + + '@webassemblyjs/helper-buffer@1.9.0': {} + + '@webassemblyjs/helper-code-frame@1.7.11': + dependencies: + '@webassemblyjs/wast-printer': 1.7.11 + + '@webassemblyjs/helper-code-frame@1.9.0': + dependencies: + '@webassemblyjs/wast-printer': 1.9.0 + + '@webassemblyjs/helper-fsm@1.7.11': {} + + '@webassemblyjs/helper-fsm@1.9.0': {} + + '@webassemblyjs/helper-module-context@1.7.11': {} + + '@webassemblyjs/helper-module-context@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + + '@webassemblyjs/helper-wasm-bytecode@1.7.11': {} + + '@webassemblyjs/helper-wasm-bytecode@1.9.0': {} + + '@webassemblyjs/helper-wasm-section@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/helper-buffer': 1.7.11 + '@webassemblyjs/helper-wasm-bytecode': 1.7.11 + '@webassemblyjs/wasm-gen': 1.7.11 + + '@webassemblyjs/helper-wasm-section@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + + '@webassemblyjs/ieee754@1.7.11': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/ieee754@1.9.0': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.7.11': + dependencies: + '@xtuc/long': 4.2.1 + + '@webassemblyjs/leb128@1.9.0': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.7.11': {} + + '@webassemblyjs/utf8@1.9.0': {} + + '@webassemblyjs/wasm-edit@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/helper-buffer': 1.7.11 + '@webassemblyjs/helper-wasm-bytecode': 1.7.11 + '@webassemblyjs/helper-wasm-section': 1.7.11 + '@webassemblyjs/wasm-gen': 1.7.11 + '@webassemblyjs/wasm-opt': 1.7.11 + '@webassemblyjs/wasm-parser': 1.7.11 + '@webassemblyjs/wast-printer': 1.7.11 + + '@webassemblyjs/wasm-edit@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/helper-wasm-section': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + '@webassemblyjs/wasm-opt': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + '@webassemblyjs/wast-printer': 1.9.0 + + '@webassemblyjs/wasm-gen@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/helper-wasm-bytecode': 1.7.11 + '@webassemblyjs/ieee754': 1.7.11 + '@webassemblyjs/leb128': 1.7.11 + '@webassemblyjs/utf8': 1.7.11 + + '@webassemblyjs/wasm-gen@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/ieee754': 1.9.0 + '@webassemblyjs/leb128': 1.9.0 + '@webassemblyjs/utf8': 1.9.0 + + '@webassemblyjs/wasm-opt@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/helper-buffer': 1.7.11 + '@webassemblyjs/wasm-gen': 1.7.11 + '@webassemblyjs/wasm-parser': 1.7.11 + + '@webassemblyjs/wasm-opt@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + + '@webassemblyjs/wasm-parser@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/helper-api-error': 1.7.11 + '@webassemblyjs/helper-wasm-bytecode': 1.7.11 + '@webassemblyjs/ieee754': 1.7.11 + '@webassemblyjs/leb128': 1.7.11 + '@webassemblyjs/utf8': 1.7.11 + + '@webassemblyjs/wasm-parser@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-api-error': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/ieee754': 1.9.0 + '@webassemblyjs/leb128': 1.9.0 + '@webassemblyjs/utf8': 1.9.0 + + '@webassemblyjs/wast-parser@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/floating-point-hex-parser': 1.7.11 + '@webassemblyjs/helper-api-error': 1.7.11 + '@webassemblyjs/helper-code-frame': 1.7.11 + '@webassemblyjs/helper-fsm': 1.7.11 + '@xtuc/long': 4.2.1 + + '@webassemblyjs/wast-parser@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/floating-point-hex-parser': 1.9.0 + '@webassemblyjs/helper-api-error': 1.9.0 + '@webassemblyjs/helper-code-frame': 1.9.0 + '@webassemblyjs/helper-fsm': 1.9.0 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/wast-printer@1.7.11': + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/wast-parser': 1.7.11 + '@xtuc/long': 4.2.1 + + '@webassemblyjs/wast-printer@1.9.0': + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/wast-parser': 1.9.0 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.1': {} + + '@xtuc/long@4.2.2': {} + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abab@2.0.6: {} + + abbrev@2.0.0: {} + + accepts@1.2.13: + dependencies: + mime-types: 2.1.35 + negotiator: 0.5.3 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-dynamic-import@3.0.0: + dependencies: + acorn: 5.7.4 + + acorn-es7-plugin@1.1.7: {} + + acorn-globals@4.3.4: + dependencies: + acorn: 6.4.2 + acorn-walk: 6.2.0 + + acorn-globals@6.0.0: + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + + acorn-jsx@3.0.1: + dependencies: + acorn: 3.3.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@6.2.0: {} + + acorn-walk@7.2.0: {} + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@3.3.0: {} + + acorn@5.7.4: {} + + acorn@6.4.2: {} + + acorn@7.4.1: {} + + acorn@8.16.0: {} + + add-dom-event-listener@1.1.0: + dependencies: + object-assign: 4.1.1 + + add-stream@1.0.0: {} + + address@1.2.2: {} + + adm-zip@0.5.17: {} + + after@0.8.1: {} + + after@0.8.2: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ajv-errors@1.0.1(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-keywords@2.1.1(ajv@5.5.2): + dependencies: + ajv: 5.5.2 + + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv@5.5.2: + dependencies: + co: 4.6.0 + fast-deep-equal: 1.1.0 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.3.1 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alphanum-sort@1.0.2: {} + + amdefine@1.0.1: {} + + ansi-align@2.0.0: + dependencies: + string-width: 2.1.1 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@3.2.3: {} + + ansi-colors@3.2.4: {} + + ansi-colors@4.1.3: {} + + ansi-escapes@3.2.0: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-html-community@0.0.8: {} + + ansi-regex@2.1.1: {} + + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@2.2.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ant-design-dtinsight-theme@1.1.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + antd: 3.26.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + transitivePeerDependencies: + - react + - react-dom + + antd@3.26.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@ant-design/create-react-context': 0.2.6(prop-types@15.8.1)(react@16.9.0) + '@ant-design/icons': 2.1.1 + '@ant-design/icons-react': 2.0.1(@ant-design/icons@2.1.1)(react@16.9.0) + '@types/react-slick': 0.23.13 + array-tree-filter: 2.1.0 + babel-runtime: 6.26.0 + classnames: 2.2.6 + copy-to-clipboard: 3.3.3 + css-animation: 1.6.1 + dom-closest: 0.2.0 + enquire.js: 2.1.6 + is-mobile: 2.2.2 + lodash: 4.18.1 + moment: 2.30.1 + omit.js: 1.0.2 + prop-types: 15.8.1 + raf: 3.4.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-calendar: 9.15.11(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-cascader: 0.17.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-checkbox: 2.1.8 + rc-collapse: 1.11.8(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-dialog: 7.6.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-drawer: 3.1.3(react@16.9.0) + rc-dropdown: 2.4.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-editor-mention: 1.1.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-form: 2.4.12(prop-types@15.8.1) + rc-input-number: 4.5.9 + rc-mentions: 0.4.2(prop-types@15.8.1)(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-menu: 7.5.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-notification: 3.3.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-pagination: 1.20.15 + rc-progress: 2.5.3 + rc-rate: 2.5.1 + rc-resize-observer: 0.1.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-select: 9.2.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-slider: 8.7.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-steps: 3.5.0 + rc-switch: 1.9.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-table: 6.10.15(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tabs: 9.7.0(react@16.9.0) + rc-time-picker: 3.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tooltip: 3.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tree: 2.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tree-select: 2.9.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-upload: 2.9.4 + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-lazy-load: 3.1.14(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react-lifecycles-compat: 3.0.4 + react-slick: 0.25.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + resize-observer-polyfill: 1.5.1 + shallowequal: 1.1.0 + warning: 4.0.3 + + antd@4.15.6(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@ant-design/colors': 6.0.0 + '@ant-design/icons': 4.8.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + '@ant-design/react-slick': 0.28.4(react@16.9.0) + '@babel/runtime': 7.29.2 + array-tree-filter: 2.1.0 + classnames: 2.5.1 + copy-to-clipboard: 3.3.3 + lodash: 4.18.1 + moment: 2.30.1 + rc-cascader: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-checkbox: 2.3.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-collapse: 3.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-dialog: 8.5.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-drawer: 4.3.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-dropdown: 3.2.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-field-form: 1.20.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-image: 5.2.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-input-number: 7.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-mentions: 1.5.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-menu: 8.10.8(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-notification: 4.5.7(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-pagination: 3.1.17(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-picker: 2.5.19(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-progress: 3.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-rate: 2.9.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-resize-observer: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-select: 12.1.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-slider: 9.7.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-steps: 4.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-switch: 3.2.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-table: 7.13.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tabs: 11.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-textarea: 0.3.7(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tooltip: 5.1.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tree: 4.1.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tree-select: 4.3.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-upload: 4.3.6(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + scroll-into-view-if-needed: 2.2.31 + warning: 4.0.3 + + any-promise@1.3.0: {} + + anymatch@2.0.0: + dependencies: + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + optional: true + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + aproba@1.2.0: {} + + archive-tool@1.0.4: + dependencies: + archiver: 2.1.1 + chalk: 2.4.2 + date-fns: 1.30.1 + fs-extra: 5.0.0 + lodash.merge: 4.6.2 + nodeinstall: 0.1.6 + shelljs: 0.8.5 + transitivePeerDependencies: + - proxy-agent + - supports-color + + archiver-utils@1.3.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 2.1.1 + readable-stream: 2.3.8 + + archiver@2.1.1: + dependencies: + archiver-utils: 1.3.0 + async: 2.6.4 + buffer-crc32: 0.2.13 + glob: 7.2.3 + lodash: 4.18.1 + readable-stream: 2.3.8 + tar-stream: 1.6.2 + zip-stream: 1.2.0 + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@4.2.2: + dependencies: + '@babel/runtime': 7.29.2 + '@babel/runtime-corejs3': 7.29.2 + + aria-query@5.3.2: {} + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-differ@1.0.0: {} + + array-equal@1.0.2: {} + + array-filter@1.0.0: {} + + array-find-index@1.0.2: {} + + array-find@1.0.0: {} + + array-flatten@1.1.1: {} + + array-ify@1.0.0: {} + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-tree-filter@2.1.0: {} + + array-union@1.0.2: + dependencies: + array-uniq: 1.0.3 + + array-union@2.1.0: {} + + array-uniq@1.0.3: {} + + array-unique@0.3.2: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.reduce@1.0.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-array-method-boxes-properly: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + is-string: 1.1.1 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + arraybuffer.slice@0.0.6: {} + + arraybuffer.slice@0.0.7: {} + + arrify@1.0.1: {} + + art-template@4.13.4: + dependencies: + acorn: 5.7.4 + escodegen: 1.14.3 + estraverse: 4.3.0 + html-minifier: 3.5.21 + is-keyword-js: 1.0.3 + js-tokens: 3.0.2 + merge-source-map: 1.1.0 + source-map: 0.5.7 + + asap@2.0.6: {} + + asn1.js@4.10.1: + dependencies: + bn.js: 4.12.3 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + assert@1.5.1: + dependencies: + object.assign: 4.1.7 + util: 0.10.4 + + assign-symbols@1.0.0: {} + + ast-types-flow@0.0.7: {} + + ast-types-flow@0.0.8: {} + + astral-regex@2.0.0: {} + + async-each@1.0.6: + optional: true + + async-function@1.0.0: {} + + async-limiter@1.0.1: {} + + async-validator@1.11.5: {} + + async-validator@3.5.2: {} + + async@1.5.2: {} + + async@2.6.4: + dependencies: + lodash: 4.18.1 + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + atob@2.1.2: {} + + autoprefixer@9.8.8: + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + normalize-range: 0.1.2 + num2fraction: 1.2.2 + picocolors: 0.2.1 + postcss: 7.0.39 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + await-event@2.1.0: {} + + await-first@1.0.0: + dependencies: + ee-first: 1.1.1 + + await-stream-ready@1.0.1: {} + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + axe-core@4.11.4: {} + + axios@0.19.2: + dependencies: + follow-redirects: 1.5.10 + transitivePeerDependencies: + - supports-color + + axobject-query@2.2.0: {} + + axobject-query@4.1.0: {} + + babel-code-frame@6.26.0: + dependencies: + chalk: 1.1.3 + esutils: 2.0.3 + js-tokens: 3.0.2 + + babel-core@6.26.3: + dependencies: + babel-code-frame: 6.26.0 + babel-generator: 6.26.1 + babel-helpers: 6.24.1 + babel-messages: 6.23.0 + babel-register: 6.26.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + convert-source-map: 1.9.0 + debug: 2.6.9 + json5: 0.5.1 + lodash: 4.18.1 + minimatch: 3.1.5 + path-is-absolute: 1.0.1 + private: 0.1.8 + slash: 1.0.0 + source-map: 0.5.7 + transitivePeerDependencies: + - supports-color + + babel-eslint@10.1.0(eslint@8.22.0): + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + eslint: 8.22.0 + eslint-visitor-keys: 1.3.0 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + babel-eslint@7.2.3: + dependencies: + babel-code-frame: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + transitivePeerDependencies: + - supports-color + + babel-eslint@8.2.6: + dependencies: + '@babel/code-frame': 7.0.0-beta.44 + '@babel/traverse': 7.0.0-beta.44 + '@babel/types': 7.0.0-beta.44 + babylon: 7.0.0-beta.44 + eslint-scope: 3.7.1 + eslint-visitor-keys: 1.3.0 + transitivePeerDependencies: + - supports-color + + babel-generator@6.26.1: + dependencies: + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + detect-indent: 4.0.0 + jsesc: 1.3.0 + lodash: 4.18.1 + source-map: 0.5.7 + trim-right: 1.0.1 + + babel-helper-bindify-decorators@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-builder-binary-assignment-operator-visitor@6.24.1: + dependencies: + babel-helper-explode-assignable-expression: 6.24.1 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-builder-react-jsx@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + esutils: 2.0.3 + + babel-helper-call-delegate@6.24.1: + dependencies: + babel-helper-hoist-variables: 6.24.1 + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-define-map@6.26.0: + dependencies: + babel-helper-function-name: 6.24.1 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + lodash: 4.18.1 + transitivePeerDependencies: + - supports-color + + babel-helper-explode-assignable-expression@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-explode-class@6.24.1: + dependencies: + babel-helper-bindify-decorators: 6.24.1 + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-function-name@6.24.1: + dependencies: + babel-helper-get-function-arity: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-get-function-arity@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-helper-hoist-variables@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-helper-optimise-call-expression@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-helper-regex@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + lodash: 4.18.1 + + babel-helper-remap-async-to-generator@6.24.1: + dependencies: + babel-helper-function-name: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helper-replace-supers@6.24.1: + dependencies: + babel-helper-optimise-call-expression: 6.24.1 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-helpers@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-loader@7.1.5(babel-core@6.26.3)(webpack@4.47.0): + dependencies: + babel-core: 6.26.3 + find-cache-dir: 1.0.0 + loader-utils: 1.4.2 + mkdirp: 0.5.6 + webpack: 4.47.0 + + babel-messages@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-add-module-exports@0.2.1: {} + + babel-plugin-check-es2015-constants@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-import@1.13.8: + dependencies: + '@babel/helper-module-imports': 7.28.6 + transitivePeerDependencies: + - supports-color + + babel-plugin-syntax-async-functions@6.13.0: {} + + babel-plugin-syntax-async-generators@6.13.0: {} + + babel-plugin-syntax-class-constructor-call@6.18.0: {} + + babel-plugin-syntax-class-properties@6.13.0: {} + + babel-plugin-syntax-decorators@6.13.0: {} + + babel-plugin-syntax-do-expressions@6.13.0: {} + + babel-plugin-syntax-dynamic-import@6.18.0: {} + + babel-plugin-syntax-exponentiation-operator@6.13.0: {} + + babel-plugin-syntax-export-extensions@6.13.0: {} + + babel-plugin-syntax-flow@6.18.0: {} + + babel-plugin-syntax-function-bind@6.13.0: {} + + babel-plugin-syntax-jsx@6.18.0: {} + + babel-plugin-syntax-object-rest-spread@6.13.0: {} + + babel-plugin-syntax-trailing-function-commas@6.22.0: {} + + babel-plugin-transform-async-generator-functions@6.24.1: + dependencies: + babel-helper-remap-async-to-generator: 6.24.1 + babel-plugin-syntax-async-generators: 6.13.0 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-async-to-generator@6.24.1: + dependencies: + babel-helper-remap-async-to-generator: 6.24.1 + babel-plugin-syntax-async-functions: 6.13.0 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-class-constructor-call@6.24.1: + dependencies: + babel-plugin-syntax-class-constructor-call: 6.18.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-class-properties@6.24.1: + dependencies: + babel-helper-function-name: 6.24.1 + babel-plugin-syntax-class-properties: 6.13.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-decorators-legacy@1.3.5: + dependencies: + babel-plugin-syntax-decorators: 6.13.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-decorators@6.24.1: + dependencies: + babel-helper-explode-class: 6.24.1 + babel-plugin-syntax-decorators: 6.13.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-do-expressions@6.22.0: + dependencies: + babel-plugin-syntax-do-expressions: 6.13.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-arrow-functions@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-block-scoped-functions@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-block-scoping@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + lodash: 4.18.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-classes@6.24.1: + dependencies: + babel-helper-define-map: 6.26.0 + babel-helper-function-name: 6.24.1 + babel-helper-optimise-call-expression: 6.24.1 + babel-helper-replace-supers: 6.24.1 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-computed-properties@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-destructuring@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-duplicate-keys@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-plugin-transform-es2015-for-of@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-function-name@6.24.1: + dependencies: + babel-helper-function-name: 6.24.1 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-literals@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-modules-amd@6.24.1: + dependencies: + babel-plugin-transform-es2015-modules-commonjs: 6.26.2 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-modules-commonjs@6.26.2: + dependencies: + babel-plugin-transform-strict-mode: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-modules-systemjs@6.24.1: + dependencies: + babel-helper-hoist-variables: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-modules-umd@6.24.1: + dependencies: + babel-plugin-transform-es2015-modules-amd: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-object-super@6.24.1: + dependencies: + babel-helper-replace-supers: 6.24.1 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-parameters@6.24.1: + dependencies: + babel-helper-call-delegate: 6.24.1 + babel-helper-get-function-arity: 6.24.1 + babel-runtime: 6.26.0 + babel-template: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-es2015-shorthand-properties@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-plugin-transform-es2015-spread@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-sticky-regex@6.24.1: + dependencies: + babel-helper-regex: 6.26.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-plugin-transform-es2015-template-literals@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-typeof-symbol@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-es2015-unicode-regex@6.24.1: + dependencies: + babel-helper-regex: 6.26.0 + babel-runtime: 6.26.0 + regexpu-core: 2.0.0 + + babel-plugin-transform-exponentiation-operator@6.24.1: + dependencies: + babel-helper-builder-binary-assignment-operator-visitor: 6.24.1 + babel-plugin-syntax-exponentiation-operator: 6.13.0 + babel-runtime: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-export-extensions@6.22.0: + dependencies: + babel-plugin-syntax-export-extensions: 6.13.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-flow-strip-types@6.22.0: + dependencies: + babel-plugin-syntax-flow: 6.18.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-function-bind@6.22.0: + dependencies: + babel-plugin-syntax-function-bind: 6.13.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-object-assign@6.22.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-object-rest-spread@6.26.0: + dependencies: + babel-plugin-syntax-object-rest-spread: 6.13.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-react-display-name@6.25.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-react-jsx-self@6.22.0: + dependencies: + babel-plugin-syntax-jsx: 6.18.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-react-jsx-source@6.22.0: + dependencies: + babel-plugin-syntax-jsx: 6.18.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-react-jsx@6.24.1: + dependencies: + babel-helper-builder-react-jsx: 6.26.0 + babel-plugin-syntax-jsx: 6.18.0 + babel-runtime: 6.26.0 + + babel-plugin-transform-regenerator@6.26.0: + dependencies: + regenerator-transform: 0.10.1 + + babel-plugin-transform-runtime@6.23.0: + dependencies: + babel-runtime: 6.26.0 + + babel-plugin-transform-strict-mode@6.24.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + + babel-polyfill@6.26.0: + dependencies: + babel-runtime: 6.26.0 + core-js: 2.6.12 + regenerator-runtime: 0.10.5 + + babel-preset-env@1.7.0: + dependencies: + babel-plugin-check-es2015-constants: 6.22.0 + babel-plugin-syntax-trailing-function-commas: 6.22.0 + babel-plugin-transform-async-to-generator: 6.24.1 + babel-plugin-transform-es2015-arrow-functions: 6.22.0 + babel-plugin-transform-es2015-block-scoped-functions: 6.22.0 + babel-plugin-transform-es2015-block-scoping: 6.26.0 + babel-plugin-transform-es2015-classes: 6.24.1 + babel-plugin-transform-es2015-computed-properties: 6.24.1 + babel-plugin-transform-es2015-destructuring: 6.23.0 + babel-plugin-transform-es2015-duplicate-keys: 6.24.1 + babel-plugin-transform-es2015-for-of: 6.23.0 + babel-plugin-transform-es2015-function-name: 6.24.1 + babel-plugin-transform-es2015-literals: 6.22.0 + babel-plugin-transform-es2015-modules-amd: 6.24.1 + babel-plugin-transform-es2015-modules-commonjs: 6.26.2 + babel-plugin-transform-es2015-modules-systemjs: 6.24.1 + babel-plugin-transform-es2015-modules-umd: 6.24.1 + babel-plugin-transform-es2015-object-super: 6.24.1 + babel-plugin-transform-es2015-parameters: 6.24.1 + babel-plugin-transform-es2015-shorthand-properties: 6.24.1 + babel-plugin-transform-es2015-spread: 6.22.0 + babel-plugin-transform-es2015-sticky-regex: 6.24.1 + babel-plugin-transform-es2015-template-literals: 6.22.0 + babel-plugin-transform-es2015-typeof-symbol: 6.23.0 + babel-plugin-transform-es2015-unicode-regex: 6.24.1 + babel-plugin-transform-exponentiation-operator: 6.24.1 + babel-plugin-transform-regenerator: 6.26.0 + browserslist: 3.2.8 + invariant: 2.2.4 + semver: 5.7.2 + transitivePeerDependencies: + - supports-color + + babel-preset-flow@6.23.0: + dependencies: + babel-plugin-transform-flow-strip-types: 6.22.0 + + babel-preset-react@6.24.1: + dependencies: + babel-plugin-syntax-jsx: 6.18.0 + babel-plugin-transform-react-display-name: 6.25.0 + babel-plugin-transform-react-jsx: 6.24.1 + babel-plugin-transform-react-jsx-self: 6.22.0 + babel-plugin-transform-react-jsx-source: 6.22.0 + babel-preset-flow: 6.23.0 + + babel-preset-stage-0@6.24.1: + dependencies: + babel-plugin-transform-do-expressions: 6.22.0 + babel-plugin-transform-function-bind: 6.22.0 + babel-preset-stage-1: 6.24.1 + transitivePeerDependencies: + - supports-color + + babel-preset-stage-1@6.24.1: + dependencies: + babel-plugin-transform-class-constructor-call: 6.24.1 + babel-plugin-transform-export-extensions: 6.22.0 + babel-preset-stage-2: 6.24.1 + transitivePeerDependencies: + - supports-color + + babel-preset-stage-2@6.24.1: + dependencies: + babel-plugin-syntax-dynamic-import: 6.18.0 + babel-plugin-transform-class-properties: 6.24.1 + babel-plugin-transform-decorators: 6.24.1 + babel-preset-stage-3: 6.24.1 + transitivePeerDependencies: + - supports-color + + babel-preset-stage-3@6.24.1: + dependencies: + babel-plugin-syntax-trailing-function-commas: 6.22.0 + babel-plugin-transform-async-generator-functions: 6.24.1 + babel-plugin-transform-async-to-generator: 6.24.1 + babel-plugin-transform-exponentiation-operator: 6.24.1 + babel-plugin-transform-object-rest-spread: 6.26.0 + transitivePeerDependencies: + - supports-color + + babel-register@6.26.0: + dependencies: + babel-core: 6.26.3 + babel-runtime: 6.26.0 + core-js: 2.6.12 + home-or-tmp: 2.0.0 + lodash: 4.18.1 + mkdirp: 0.5.6 + source-map-support: 0.4.18 + transitivePeerDependencies: + - supports-color + + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + + babel-template@6.26.0: + dependencies: + babel-runtime: 6.26.0 + babel-traverse: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + lodash: 4.18.1 + transitivePeerDependencies: + - supports-color + + babel-traverse@6.26.0: + dependencies: + babel-code-frame: 6.26.0 + babel-messages: 6.23.0 + babel-runtime: 6.26.0 + babel-types: 6.26.0 + babylon: 6.18.0 + debug: 2.6.9 + globals: 9.18.0 + invariant: 2.2.4 + lodash: 4.18.1 + transitivePeerDependencies: + - supports-color + + babel-types@6.26.0: + dependencies: + babel-runtime: 6.26.0 + esutils: 2.0.3 + lodash: 4.18.1 + to-fast-properties: 1.0.3 + + babel-upgrade@1.0.1: + dependencies: + '@babel/polyfill': 7.12.1 + commander: 2.20.3 + cross-spawn: 6.0.6 + diff: 4.0.4 + globby: 9.2.0 + has-yarn: 2.1.0 + json5: 2.2.3 + pify: 4.0.1 + read-pkg-up: 5.0.0 + semver: 5.7.2 + sort-keys: 2.0.0 + write: 1.0.3 + write-json-file: 3.2.0 + write-pkg: 3.2.0 + transitivePeerDependencies: + - supports-color + + babylon@6.18.0: {} + + babylon@7.0.0-beta.44: {} + + backo2@1.0.2: {} + + bail@1.0.5: {} + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + base64-arraybuffer@0.1.4: {} + + base64-arraybuffer@0.1.5: {} + + base64-js@1.5.1: {} + + base64-url@1.2.1: {} + + base64id@2.0.0: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + baseline-browser-mapping@2.10.31: {} + + basic-auth-connect@1.0.0: {} + + basic-auth@1.0.4: {} + + batch@0.5.3: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + better-assert@1.0.2: + dependencies: + callsite: 1.0.0 + + bfj@6.1.2: + dependencies: + bluebird: 3.7.2 + check-types: 8.0.3 + hoopy: 0.1.4 + tryer: 1.0.1 + + big.js@3.2.0: {} + + big.js@5.2.2: {} + + binary-extensions@1.13.1: + optional: true + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + + bl@1.2.3: + dependencies: + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + black-hole-stream@0.0.1: {} + + blob@0.0.4: {} + + blob@0.0.5: {} + + block-stream@0.0.9: + dependencies: + inherits: 2.0.4 + + bluebird@3.7.2: {} + + bn.js@4.12.3: {} + + bn.js@5.2.3: {} + + body-parser@1.13.3: + dependencies: + bytes: 2.1.0 + content-type: 1.0.5 + debug: 2.2.0 + depd: 1.0.1 + http-errors: 1.3.1 + iconv-lite: 0.4.11 + on-finished: 2.3.0 + qs: 4.0.0 + raw-body: 2.1.7 + type-is: 1.6.18 + transitivePeerDependencies: + - supports-color + + body-parser@1.20.5: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.0: {} + + boxen@1.3.0: + dependencies: + ansi-align: 2.0.0 + camelcase: 4.1.0 + chalk: 2.4.2 + cli-boxes: 1.0.0 + string-width: 2.1.1 + term-size: 1.2.0 + widest-line: 2.0.1 + + boxen@4.2.0: + dependencies: + ansi-align: 3.0.1 + camelcase: 5.3.1 + chalk: 3.0.0 + cli-boxes: 2.2.1 + string-width: 4.2.3 + term-size: 2.2.1 + type-fest: 0.8.1 + widest-line: 3.1.0 + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + brorand@1.1.0: {} + + browser-process-hrtime@1.0.0: {} + + browser-stdout@1.3.1: {} + + browserify-aes@1.2.0: + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.7 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + browserify-cipher@1.0.1: + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + + browserify-des@1.0.2: + dependencies: + cipher-base: 1.0.7 + des.js: 1.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + browserify-rsa@4.1.1: + dependencies: + bn.js: 5.2.3 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + browserify-sign@4.2.5: + dependencies: + bn.js: 5.2.3 + browserify-rsa: 4.1.1 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.6.1 + inherits: 2.0.4 + parse-asn1: 5.1.9 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + + browserslist@3.2.8: + dependencies: + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.360 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.360 + node-releases: 2.0.45 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-alloc-unsafe@1.1.0: {} + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + + buffer-crc32@0.2.13: {} + + buffer-fill@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer-xor@1.0.3: {} + + buffer@4.9.2: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buildcheck@0.0.7: + optional: true + + builtin-status-codes@3.0.0: {} + + builtins@5.1.0: + dependencies: + semver: 7.8.0 + + busboy@0.2.14: + dependencies: + dicer: 0.2.5 + readable-stream: 1.1.14 + + byte@2.0.0: + dependencies: + debug: 3.2.7 + long: 4.0.0 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + + bytes@2.1.0: {} + + bytes@2.2.0: {} + + bytes@2.4.0: {} + + bytes@2.5.0: {} + + bytes@3.1.2: {} + + c8@7.14.0: + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@istanbuljs/schema': 0.1.6 + find-up: 5.0.0 + foreground-child: 2.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + rimraf: 3.0.2 + test-exclude: 6.0.0 + v8-to-istanbul: 9.3.0 + yargs: 16.2.0 + yargs-parser: 20.2.9 + + cacache@10.0.4: + dependencies: + bluebird: 3.7.2 + chownr: 1.1.4 + glob: 7.2.3 + graceful-fs: 4.2.11 + lru-cache: 4.1.5 + mississippi: 2.0.0 + mkdirp: 0.5.6 + move-concurrently: 1.0.1 + promise-inflight: 1.0.1(bluebird@3.7.2) + rimraf: 2.7.1 + ssri: 5.3.0 + unique-filename: 1.1.1 + y18n: 4.0.3 + + cacache@12.0.4: + dependencies: + bluebird: 3.7.2 + chownr: 1.1.4 + figgy-pudding: 3.5.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + infer-owner: 1.0.4 + lru-cache: 5.1.1 + mississippi: 3.0.0 + mkdirp: 0.5.6 + move-concurrently: 1.0.1 + promise-inflight: 1.0.1(bluebird@3.7.2) + rimraf: 2.7.1 + ssri: 6.0.2 + unique-filename: 1.1.1 + y18n: 4.0.3 + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + cache-content-type@1.0.1: + dependencies: + mime-types: 2.1.35 + ylru: 1.4.0 + + cache-loader@1.2.5(webpack@4.47.0): + dependencies: + loader-utils: 1.4.2 + mkdirp: 0.5.6 + neo-async: 2.6.2 + schema-utils: 0.4.7 + webpack: 4.47.0 + + cacheable-request@6.1.0: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + + cachedir@2.3.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + call-matcher@1.1.0: + dependencies: + core-js: 2.6.12 + deep-equal: 1.1.2 + espurify: 1.8.1 + estraverse: 4.3.0 + + call-me-maybe@1.0.2: {} + + call-signature@0.0.2: {} + + caller-callsite@2.0.0: + dependencies: + callsites: 2.0.0 + + caller-path@0.1.0: + dependencies: + callsites: 0.2.0 + + caller-path@2.0.0: + dependencies: + caller-callsite: 2.0.0 + + callsite@1.0.0: {} + + callsites@0.2.0: {} + + callsites@2.0.0: {} + + callsites@3.1.0: {} + + camel-case@3.0.0: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + camelcase-keys@2.1.0: + dependencies: + camelcase: 2.1.1 + map-obj: 1.0.1 + + camelcase-keys@4.2.0: + dependencies: + camelcase: 4.1.0 + map-obj: 2.0.0 + quick-lru: 1.1.0 + + camelcase-keys@6.2.2: + dependencies: + camelcase: 5.3.1 + map-obj: 4.3.0 + quick-lru: 4.0.1 + + camelcase@2.1.1: {} + + camelcase@3.0.0: {} + + camelcase@4.1.0: {} + + camelcase@5.3.1: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001793: {} + + capture-stack-trace@1.0.2: {} + + case-sensitive-paths-webpack-plugin@2.4.0: {} + + caseless@0.12.0: {} + + ccount@1.1.0: {} + + cfork@1.11.0: + dependencies: + utility: 1.18.0 + + chalk@1.1.3: + dependencies: + ansi-styles: 2.2.1 + escape-string-regexp: 1.0.5 + has-ansi: 2.0.0 + strip-ansi: 3.0.1 + supports-color: 2.0.0 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chan@0.6.1: {} + + change-case@3.1.0: + dependencies: + camel-case: 3.0.0 + constant-case: 2.0.0 + dot-case: 2.1.1 + header-case: 1.0.1 + is-lower-case: 1.1.3 + is-upper-case: 1.1.2 + lower-case: 1.1.4 + lower-case-first: 1.0.2 + no-case: 2.3.2 + param-case: 2.1.1 + pascal-case: 2.0.1 + path-case: 2.1.1 + sentence-case: 2.1.1 + snake-case: 2.1.0 + swap-case: 1.1.2 + title-case: 2.1.1 + upper-case: 1.1.3 + upper-case-first: 1.1.2 + + character-entities-legacy@1.1.4: {} + + character-entities@1.2.4: {} + + character-reference-invalid@1.1.4: {} + + chardet@0.4.2: {} + + chardet@0.7.0: {} + + charenc@0.0.2: {} + + check-types@8.0.3: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.25.0 + whatwg-mimetype: 4.0.0 + + chokidar@2.1.8: + dependencies: + anymatch: 2.0.0 + async-each: 1.0.6 + braces: 2.3.2 + glob-parent: 3.1.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 4.0.3 + normalize-path: 3.0.0 + path-is-absolute: 1.0.1 + readdirp: 2.2.1 + upath: 1.2.0 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + optional: true + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + chrome-trace-event@1.0.4: {} + + ci-info@1.6.0: {} + + ci-info@2.0.0: {} + + cipher-base@1.0.7: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + circular-json-for-egg@1.0.0: {} + + circular-json@0.3.3: {} + + circular-json@0.5.9: {} + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + classnames@2.2.6: {} + + classnames@2.5.1: {} + + clean-css@4.2.4: + dependencies: + source-map: 0.6.1 + + clean-webpack-plugin@0.1.19: + dependencies: + rimraf: 2.7.1 + + cli-boxes@1.0.0: {} + + cli-boxes@2.2.1: {} + + cli-color@1.4.0: + dependencies: + ansi-regex: 2.1.1 + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + memoizee: 0.4.17 + timers-ext: 0.1.8 + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-width@2.2.1: {} + + cli-width@3.0.0: {} + + cliui@3.2.0: + dependencies: + string-width: 1.0.2 + strip-ansi: 3.0.1 + wrap-ansi: 2.1.0 + + cliui@5.0.0: + dependencies: + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi: 5.1.0 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clone@1.0.4: {} + + clone@2.1.2: {} + + cls-bluebird@2.1.0: + dependencies: + is-bluebird: 1.0.2 + shimmer: 1.2.1 + + cluster-client@3.7.0: + dependencies: + byte: 2.0.0 + co: 4.6.0 + egg-logger: 3.6.1 + is-type-of: 1.4.0 + json-stringify-safe: 5.0.1 + long: 4.0.0 + sdk-base: 4.2.1 + serialize-json: 1.0.3 + tcp-base: 3.2.0 + utility: 2.5.0 + transitivePeerDependencies: + - supports-color + + cluster-reload@1.1.0: {} + + co-body@6.2.0: + dependencies: + '@hapi/bourne': 3.0.0 + inflation: 2.1.0 + qs: 6.15.2 + raw-body: 2.5.3 + type-is: 1.6.18 + + co-busboy@1.5.0: + dependencies: + black-hole-stream: 0.0.1 + busboy: 0.2.14 + chan: 0.6.1 + inflation: 2.1.0 + + co-mocha@1.2.2(mocha@6.2.3): + dependencies: + co: 4.6.0 + is-generator: 1.0.3 + mocha: 6.2.3 + + co-request@0.2.1: + dependencies: + request: 2.88.2 + + co@4.6.0: {} + + coa@2.0.2: + dependencies: + '@types/q': 1.5.8 + chalk: 2.4.2 + q: 1.5.1 + + code-point-at@1.1.0: {} + + codemirror@5.65.21: {} + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colord@2.9.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@1.0.8: {} + + commander@10.0.1: {} + + commander@14.0.3: {} + + commander@2.13.0: {} + + commander@2.17.1: {} + + commander@2.19.0: {} + + commander@2.20.3: {} + + comment-parser@0.5.5: {} + + commitizen@4.3.1(@types/node@25.9.1)(typescript@4.7.4): + dependencies: + cachedir: 2.3.0 + cz-conventional-changelog: 3.3.0(@types/node@25.9.1)(typescript@4.7.4) + dedent: 0.7.0 + detect-indent: 6.1.0 + find-node-modules: 2.1.3 + find-root: 1.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + inquirer: 8.2.5 + is-utf8: 0.2.1 + lodash: 4.17.21 + minimist: 1.2.7 + strip-bom: 4.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + commitlint@8.3.6: + dependencies: + '@commitlint/cli': 8.3.6 + read-pkg: 5.2.0 + resolve-pkg: 2.0.0 + + common-bin@2.9.2: + dependencies: + '@types/dargs': 5.1.0 + '@types/node': 10.17.60 + '@types/yargs': 12.0.20 + chalk: 2.4.2 + change-case: 3.1.0 + co: 4.6.0 + dargs: 6.1.0 + debug: 4.4.3 + is-type-of: 1.4.0 + semver: 5.7.2 + yargs: 13.3.2 + yargs-parser: 13.1.2 + transitivePeerDependencies: + - supports-color + + commondir@1.0.1: {} + + compare-func@1.3.4: + dependencies: + array-ify: 1.0.0 + dot-prop: 3.0.0 + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + component-bind@1.0.0: {} + + component-classes@1.2.6: + dependencies: + component-indexof: 0.0.3 + + component-emitter@1.1.2: {} + + component-emitter@1.2.1: {} + + component-emitter@1.3.1: {} + + component-indexof@0.0.3: {} + + component-inherit@0.0.3: {} + + composition@2.3.0: + dependencies: + any-promise: 1.3.0 + co: 4.6.0 + + compress-commons@1.2.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 2.0.0 + normalize-path: 2.1.1 + readable-stream: 2.3.8 + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compressing@1.10.5: + dependencies: + '@eggjs/yauzl': 2.11.0 + flushwritable: 1.0.0 + get-ready: 1.0.0 + iconv-lite: 0.5.2 + mkdirp: 0.5.6 + pump: 3.0.4 + streamifier: 0.1.1 + tar-stream: 1.6.2 + yazl: 2.5.1 + + compression@1.5.2: + dependencies: + accepts: 1.2.13 + bytes: 2.1.0 + compressible: 2.0.18 + debug: 2.2.0 + on-headers: 1.0.2 + vary: 1.0.1 + transitivePeerDependencies: + - supports-color + + compute-scroll-into-view@1.0.20: {} + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@3.1.5: + dependencies: + dot-prop: 4.2.1 + graceful-fs: 4.2.11 + make-dir: 1.3.0 + unique-string: 1.0.0 + write-file-atomic: 2.4.3 + xdg-basedir: 3.0.0 + + configstore@5.0.1: + dependencies: + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 + + connect-history-api-fallback@1.6.0: {} + + connect-livereload@0.6.1: {} + + connect-timeout@1.6.2: + dependencies: + debug: 2.2.0 + http-errors: 1.3.1 + ms: 0.7.1 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + + connect@2.30.2: + dependencies: + basic-auth-connect: 1.0.0 + body-parser: 1.13.3 + bytes: 2.1.0 + compression: 1.5.2 + connect-timeout: 1.6.2 + content-type: 1.0.5 + cookie: 0.1.3 + cookie-parser: 1.3.5 + cookie-signature: 1.0.6 + csurf: 1.8.3 + debug: 2.2.0 + depd: 1.0.1 + errorhandler: 1.4.3 + express-session: 1.11.3 + finalhandler: 0.4.0 + fresh: 0.3.0 + http-errors: 1.3.1 + method-override: 2.3.10 + morgan: 1.6.1 + multiparty: 3.3.2 + on-headers: 1.0.2 + parseurl: 1.3.3 + pause: 0.1.0 + qs: 4.0.0 + response-time: 2.3.4 + serve-favicon: 2.3.2 + serve-index: 1.7.3 + serve-static: 1.10.3 + type-is: 1.6.18 + utils-merge: 1.0.0 + vhost: 3.0.2 + transitivePeerDependencies: + - supports-color + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + console-browserify@1.2.0: {} + + constant-case@2.0.0: + dependencies: + snake-case: 2.1.0 + upper-case: 1.1.3 + + constants-browserify@1.0.0: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + conventional-changelog-angular@1.6.6: + dependencies: + compare-func: 1.3.4 + q: 1.5.1 + + conventional-changelog-angular@5.0.13: + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + + conventional-changelog-atom@2.0.8: + dependencies: + q: 1.5.1 + + conventional-changelog-cli@2.2.2: + dependencies: + add-stream: 1.0.0 + conventional-changelog: 3.1.25 + lodash: 4.18.1 + meow: 8.1.2 + tempfile: 3.0.0 + + conventional-changelog-codemirror@2.0.8: + dependencies: + q: 1.5.1 + + conventional-changelog-config-spec@2.1.0: {} + + conventional-changelog-conventionalcommits@4.2.1: + dependencies: + compare-func: 1.3.4 + lodash: 4.18.1 + q: 1.5.1 + + conventional-changelog-conventionalcommits@4.6.3: + dependencies: + compare-func: 2.0.0 + lodash: 4.18.1 + q: 1.5.1 + + conventional-changelog-core@4.2.4: + dependencies: + add-stream: 1.0.0 + conventional-changelog-writer: 5.0.1 + conventional-commits-parser: 3.2.4 + dateformat: 3.0.3 + get-pkg-repo: 4.2.1 + git-raw-commits: 2.0.11 + git-remote-origin-url: 2.0.0 + git-semver-tags: 4.1.1 + lodash: 4.18.1 + normalize-package-data: 3.0.3 + q: 1.5.1 + read-pkg: 3.0.0 + read-pkg-up: 3.0.0 + through2: 4.0.2 + + conventional-changelog-ember@2.0.9: + dependencies: + q: 1.5.1 + + conventional-changelog-eslint@3.0.9: + dependencies: + q: 1.5.1 + + conventional-changelog-express@2.0.6: + dependencies: + q: 1.5.1 + + conventional-changelog-jquery@3.0.11: + dependencies: + q: 1.5.1 + + conventional-changelog-jshint@2.0.9: + dependencies: + compare-func: 2.0.0 + q: 1.5.1 + + conventional-changelog-preset-loader@2.3.4: {} + + conventional-changelog-writer@5.0.1: + dependencies: + conventional-commits-filter: 2.0.7 + dateformat: 3.0.3 + handlebars: 4.7.9 + json-stringify-safe: 5.0.1 + lodash: 4.18.1 + meow: 8.1.2 + semver: 6.3.1 + split: 1.0.1 + through2: 4.0.2 + + conventional-changelog@3.1.25: + dependencies: + conventional-changelog-angular: 5.0.13 + conventional-changelog-atom: 2.0.8 + conventional-changelog-codemirror: 2.0.8 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-changelog-core: 4.2.4 + conventional-changelog-ember: 2.0.9 + conventional-changelog-eslint: 3.0.9 + conventional-changelog-express: 2.0.6 + conventional-changelog-jquery: 3.0.11 + conventional-changelog-jshint: 2.0.9 + conventional-changelog-preset-loader: 2.3.4 + + conventional-commit-types@3.0.0: {} + + conventional-commits-filter@2.0.7: + dependencies: + lodash.ismatch: 4.4.0 + modify-values: 1.0.1 + + conventional-commits-parser@3.2.4: + dependencies: + JSONStream: 1.3.5 + is-text-path: 1.0.1 + lodash: 4.18.1 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + + conventional-commits-parser@6.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + optional: true + + conventional-recommended-bump@6.1.0: + dependencies: + concat-stream: 2.0.0 + conventional-changelog-preset-loader: 2.3.4 + conventional-commits-filter: 2.0.7 + conventional-commits-parser: 3.2.4 + git-raw-commits: 2.0.11 + git-semver-tags: 4.1.1 + meow: 8.1.2 + q: 1.5.1 + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.3.5: + dependencies: + cookie: 0.1.3 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.1.3: {} + + cookie@0.4.2: {} + + cookie@0.7.2: {} + + cookies@0.8.0: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + + cookies@0.9.1: + dependencies: + depd: 2.0.0 + keygrip: 1.1.0 + + copy-concurrently@1.0.5: + dependencies: + aproba: 1.2.0 + fs-write-stream-atomic: 1.0.10 + iferr: 0.1.5 + mkdirp: 0.5.6 + rimraf: 2.7.1 + run-queue: 1.0.3 + + copy-descriptor@0.1.1: {} + + copy-text-to-clipboard@3.2.2: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + copy-to@2.0.1: {} + + copy-webpack-plugin@4.6.0: + dependencies: + cacache: 10.0.4 + find-cache-dir: 1.0.0 + globby: 7.1.1 + is-glob: 4.0.3 + loader-utils: 1.4.2 + minimatch: 3.1.5 + p-limit: 1.3.0 + serialize-javascript: 1.9.1 + + core-js-pure@3.49.0: {} + + core-js@1.2.7: {} + + core-js@2.6.12: {} + + core-js@3.49.0: {} + + core-util-is@1.0.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig-typescript-loader@6.3.0(@types/node@25.9.1)(cosmiconfig@9.0.1(typescript@4.7.4))(typescript@4.7.4): + dependencies: + '@types/node': 25.9.1 + cosmiconfig: 9.0.1(typescript@4.7.4) + jiti: 2.6.1 + typescript: 4.7.4 + optional: true + + cosmiconfig@5.2.1: + dependencies: + import-fresh: 2.0.0 + is-directory: 0.3.1 + js-yaml: 3.14.2 + parse-json: 4.0.0 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + cosmiconfig@9.0.1(typescript@4.7.4): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 4.7.4 + optional: true + + cp-file@7.0.0: + dependencies: + graceful-fs: 4.2.11 + make-dir: 3.1.0 + nested-error-stacks: 2.1.1 + p-event: 4.2.0 + + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.27.0 + optional: true + + crc32-stream@2.0.0: + dependencies: + crc: 3.8.0 + readable-stream: 2.3.8 + + crc@3.3.0: {} + + crc@3.8.0: + dependencies: + buffer: 5.7.1 + + create-ecdh@4.0.4: + dependencies: + bn.js: 4.12.3 + elliptic: 6.6.1 + + create-error-class@3.0.2: + dependencies: + capture-stack-trace: 1.0.2 + + create-hash@1.2.0: + dependencies: + cipher-base: 1.0.7 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.3 + sha.js: 2.4.12 + + create-hmac@1.1.7: + dependencies: + cipher-base: 1.0.7 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + + create-react-class@15.7.0: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + create-require@1.1.1: {} + + crequire@1.8.1: {} + + cron-parser@2.18.0: + dependencies: + is-nan: 1.3.2 + moment-timezone: 0.5.48 + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + + cropperjs@1.6.2: {} + + cross-port-killer@1.4.0: {} + + cross-spawn@5.1.0: + dependencies: + lru-cache: 4.1.5 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypt@0.0.2: {} + + crypto-browserify@3.12.1: + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.5 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + hash-base: 3.0.5 + inherits: 2.0.4 + pbkdf2: 3.1.5 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + + crypto-random-string@1.0.0: {} + + crypto-random-string@2.0.0: {} + + csrf@3.0.6: + dependencies: + rndm: 1.2.0 + tsscmp: 1.0.5 + uid-safe: 2.1.4 + + csrf@3.1.0: + dependencies: + rndm: 1.2.0 + tsscmp: 1.0.6 + uid-safe: 2.1.5 + + css-animation@1.6.1: + dependencies: + babel-runtime: 6.26.0 + component-classes: 1.2.6 + + css-color-names@0.0.4: {} + + css-declaration-sorter@4.0.1: + dependencies: + postcss: 7.0.39 + timsort: 0.3.0 + + css-functions-list@3.3.3: {} + + css-loader@3.6.0(webpack@4.47.0): + dependencies: + camelcase: 5.3.1 + cssesc: 3.0.0 + icss-utils: 4.1.1 + loader-utils: 1.4.2 + normalize-path: 3.0.0 + postcss: 7.0.39 + postcss-modules-extract-imports: 2.0.0 + postcss-modules-local-by-default: 3.0.3 + postcss-modules-scope: 2.2.0 + postcss-modules-values: 3.0.0 + postcss-value-parser: 4.2.0 + schema-utils: 2.7.1 + semver: 6.3.1 + webpack: 4.47.0 + + css-select-base-adapter@0.1.1: {} + + css-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 3.4.2 + domutils: 1.7.0 + nth-check: 1.0.2 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@1.0.0-alpha.37: + dependencies: + mdn-data: 2.0.4 + source-map: 0.6.1 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + css-what@3.4.2: {} + + css-what@6.2.2: {} + + cssesc@3.0.0: {} + + cssfilter@0.0.10: {} + + cssnano-preset-default@4.0.8: + dependencies: + css-declaration-sorter: 4.0.1 + cssnano-util-raw-cache: 4.0.1 + postcss: 7.0.39 + postcss-calc: 7.0.5 + postcss-colormin: 4.0.3 + postcss-convert-values: 4.0.1 + postcss-discard-comments: 4.0.2 + postcss-discard-duplicates: 4.0.2 + postcss-discard-empty: 4.0.1 + postcss-discard-overridden: 4.0.1 + postcss-merge-longhand: 4.0.11 + postcss-merge-rules: 4.0.3 + postcss-minify-font-values: 4.0.2 + postcss-minify-gradients: 4.0.2 + postcss-minify-params: 4.0.2 + postcss-minify-selectors: 4.0.2 + postcss-normalize-charset: 4.0.1 + postcss-normalize-display-values: 4.0.2 + postcss-normalize-positions: 4.0.2 + postcss-normalize-repeat-style: 4.0.2 + postcss-normalize-string: 4.0.2 + postcss-normalize-timing-functions: 4.0.2 + postcss-normalize-unicode: 4.0.1 + postcss-normalize-url: 4.0.1 + postcss-normalize-whitespace: 4.0.2 + postcss-ordered-values: 4.1.2 + postcss-reduce-initial: 4.0.3 + postcss-reduce-transforms: 4.0.2 + postcss-svgo: 4.0.3 + postcss-unique-selectors: 4.0.1 + + cssnano-util-get-arguments@4.0.0: {} + + cssnano-util-get-match@4.0.0: {} + + cssnano-util-raw-cache@4.0.1: + dependencies: + postcss: 7.0.39 + + cssnano-util-same-parent@4.0.1: {} + + cssnano@4.1.11: + dependencies: + cosmiconfig: 5.2.1 + cssnano-preset-default: 4.0.8 + is-resolvable: 1.1.0 + postcss: 7.0.39 + + csso@4.2.0: + dependencies: + css-tree: 1.1.3 + + cssom@0.3.8: {} + + cssom@0.4.4: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + csstype@3.2.3: {} + + csurf@1.8.3: + dependencies: + cookie: 0.1.3 + cookie-signature: 1.0.6 + csrf: 3.0.6 + http-errors: 1.3.1 + + currently-unhandled@0.4.1: + dependencies: + array-find-index: 1.0.2 + + cyclist@1.0.2: {} + + cz-conventional-changelog@3.3.0(@types/node@25.9.1)(typescript@4.7.4): + dependencies: + chalk: 2.4.2 + commitizen: 4.3.1(@types/node@25.9.1)(typescript@4.7.4) + conventional-commit-types: 3.0.0 + lodash.map: 4.6.0 + longest: 2.0.1 + word-wrap: 1.2.5 + optionalDependencies: + '@commitlint/load': 21.0.1(@types/node@25.9.1)(typescript@4.7.4) + transitivePeerDependencies: + - '@types/node' + - typescript + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + damerau-levenshtein@1.0.8: {} + + dargs@6.1.0: {} + + dargs@7.0.0: {} + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + data-urls@1.1.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 7.1.0 + + data-urls@2.0.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + date-fns@1.30.1: {} + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.29.2 + + dateformat@3.0.3: {} + + dayjs@1.11.20: {} + + debounce@1.2.1: {} + + debug@2.2.0: + dependencies: + ms: 0.7.1 + + debug@2.3.3: + dependencies: + ms: 0.7.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.1.0: + dependencies: + ms: 2.0.0 + + debug@3.2.6(supports-color@6.0.0): + dependencies: + ms: 2.1.1 + optionalDependencies: + supports-color: 6.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.1.1: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize-keys@1.1.1: + dependencies: + decamelize: 1.2.0 + map-obj: 1.0.1 + + decamelize@1.2.0: {} + + decimal.js@10.6.0: {} + + decode-uri-component@0.2.2: {} + + decompress-response@3.3.0: + dependencies: + mimic-response: 1.0.1 + + dedent@0.7.0: {} + + deep-equal@1.0.1: {} + + deep-equal@1.1.2: + dependencies: + is-arguments: 1.2.0 + is-date-object: 1.1.0 + is-regex: 1.2.1 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.4 + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + default-user-agent@1.0.0: + dependencies: + os-name: 1.0.3 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@1.1.3: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.8 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.4 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.4 + isobject: 3.0.1 + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + denque@1.5.1: {} + + depd@1.0.1: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + des.js@1.1.0: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + destroy@1.0.4: {} + + destroy@1.2.0: {} + + detect-file@1.0.0: {} + + detect-indent@4.0.0: + dependencies: + repeating: 2.0.1 + + detect-indent@5.0.0: {} + + detect-indent@6.1.0: {} + + detect-libc@2.1.2: + optional: true + + detect-newline@3.1.0: {} + + detect-port@1.6.1: + dependencies: + address: 1.2.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + dicer@0.2.5: + dependencies: + readable-stream: 1.1.14 + streamsearch: 0.1.2 + + diff-match-patch@1.0.5: {} + + diff@3.5.0: {} + + diff@3.5.1: {} + + diff@4.0.4: {} + + diffie-hellman@5.0.3: + dependencies: + bn.js: 4.12.3 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + + digest-header@1.1.0: {} + + dingtalk-robot-sender@1.2.0: + dependencies: + axios: 0.19.2 + transitivePeerDependencies: + - supports-color + + dir-glob@2.2.2: + dependencies: + path-type: 3.0.0 + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + directory-named-webpack-plugin@2.3.0(webpack@4.47.0): + dependencies: + enhanced-resolve: 3.4.1 + object-assign: 4.1.1 + webpack: 4.47.0 + + docsify-cli@4.4.4(encoding@0.1.13): + dependencies: + chalk: 2.4.2 + connect: 3.7.0 + connect-history-api-fallback: 1.6.0 + connect-livereload: 0.6.1 + cp-file: 7.0.0 + docsify: 4.13.1 + docsify-server-renderer: 4.13.1(encoding@0.1.13) + enquirer: 2.4.1 + fs-extra: 8.1.0 + get-port: 5.1.1 + livereload: 0.9.3 + lru-cache: 5.1.1 + open: 6.4.0 + serve-static: 1.16.3 + update-notifier: 4.1.3 + yargonaut: 1.1.4 + yargs: 15.4.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + docsify-server-renderer@4.13.1(encoding@0.1.13): + dependencies: + debug: 4.4.3 + docsify: 4.13.1 + node-fetch: 2.7.0(encoding@0.1.13) + resolve-pathname: 3.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + docsify@4.13.1: + dependencies: + marked: 1.2.9 + medium-zoom: 1.1.0 + opencollective-postinstall: 2.0.3 + prismjs: 1.30.0 + strip-indent: 3.0.0 + tinydate: 1.3.0 + tweezer.js: 1.5.0 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-align@1.12.4: {} + + dom-closest@0.2.0: + dependencies: + dom-matches: 2.0.0 + + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + + dom-matches@2.0.0: {} + + dom-scroll-into-view@1.2.1: {} + + dom-serializer@0.2.2: + dependencies: + domelementtype: 2.3.0 + entities: 2.2.0 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom-urls@1.1.0: + dependencies: + urijs: 1.19.11 + + dom-walk@0.1.2: {} + + domain-browser@1.2.0: {} + + domelementtype@1.3.1: {} + + domelementtype@2.3.0: {} + + domexception@1.0.1: + dependencies: + webidl-conversions: 4.0.2 + + domexception@2.0.1: + dependencies: + webidl-conversions: 5.0.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@1.7.0: + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@2.1.1: + dependencies: + no-case: 2.3.2 + + dot-prop@3.0.0: + dependencies: + is-obj: 1.0.1 + + dot-prop@4.2.1: + dependencies: + is-obj: 1.0.1 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotgitignore@2.1.0: + dependencies: + find-up: 3.0.0 + minimatch: 3.1.5 + + dottie@2.0.7: {} + + draft-js@0.10.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + fbjs: 0.8.18 + immutable: 3.7.6 + object-assign: 4.1.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer3@0.1.5: {} + + duplexer@0.1.2: {} + + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + eastasianwidth@0.2.0: {} + + easy-helper@1.0.2: + dependencies: + node-tool-utils: 1.6.0 + webpack-merge: 4.2.2 + + easy-puppeteer-html@1.0.2: + dependencies: + chalk: 4.1.2 + jsdom: 16.7.0 + urllib: 2.44.0 + transitivePeerDependencies: + - bufferutil + - canvas + - proxy-agent + - supports-color + - utf-8-validate + + easywebpack-cli@4.8.1(webpack@4.47.0): + dependencies: + archive-tool: 1.0.4 + babel-upgrade: 1.0.1 + chalk: 2.4.2 + commander: 2.20.3 + compressing: 1.10.5 + easy-puppeteer-html: 1.0.2 + execa: 1.0.0 + inquirer: 7.3.3 + jsdom: 15.2.1 + lodash.get: 4.4.2 + mz-modules: 1.0.0 + node-glob: 1.2.0 + node-tool-utils: 1.6.0 + ora: 3.4.0 + progress-bar-webpack-plugin: 1.12.1(webpack@4.47.0) + shelljs: 0.7.8 + speed-measure-webpack-plugin: 1.6.0(webpack@4.47.0) + stats-webpack-plugin: 0.7.0(webpack@4.47.0) + urllib: 2.44.0 + webpack-bundle-analyzer: 3.9.0 + webpack-tool: 4.5.4 + transitivePeerDependencies: + - bufferutil + - canvas + - debug + - proxy-agent + - supports-color + - utf-8-validate + - webpack + - webpack-cli + - webpack-command + + easywebpack-react@4.4.5(@types/react@16.14.70)(eslint@8.22.0)(react-dom@16.14.0(react@16.9.0))(react@16.9.0)(typescript@4.7.4)(webpack@4.47.0): + dependencies: + '@hot-loader/react-dom': 16.14.0(react@16.9.0) + babel-preset-react: 6.24.1 + easywebpack: 4.12.8(typescript@4.7.4)(webpack@4.47.0) + eslint-plugin-react: 7.37.5(eslint@8.22.0) + isomorphic-style-loader: 4.0.0 + react-entry-template-loader: 1.0.3 + react-hot-loader: 4.13.1(@types/react@16.14.70)(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + transitivePeerDependencies: + - '@types/react' + - debug + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - react + - react-dom + - supports-color + - typescript + - webpack + - webpack-cli + - webpack-command + + easywebpack@4.12.8(typescript@4.7.4)(webpack@4.47.0): + dependencies: + autoprefixer: 9.8.8 + babel-core: 6.26.3 + babel-eslint: 8.2.6 + babel-loader: 7.1.5(babel-core@6.26.3)(webpack@4.47.0) + babel-plugin-add-module-exports: 0.2.1 + babel-plugin-import: 1.13.8 + babel-plugin-syntax-dynamic-import: 6.18.0 + babel-plugin-transform-object-assign: 6.22.0 + babel-plugin-transform-object-rest-spread: 6.26.0 + babel-plugin-transform-runtime: 6.23.0 + babel-preset-env: 1.7.0 + cache-loader: 1.2.5(webpack@4.47.0) + case-sensitive-paths-webpack-plugin: 2.4.0 + chalk: 2.4.2 + clean-webpack-plugin: 0.1.19 + copy-webpack-plugin: 4.6.0 + cross-spawn: 5.1.0 + css-loader: 3.6.0(webpack@4.47.0) + detect-port: 1.6.1 + directory-named-webpack-plugin: 2.3.0(webpack@4.47.0) + eslint: 4.19.1 + eslint-config-egg: 7.5.1(eslint@4.19.1)(typescript@4.7.4) + eslint-loader: 2.2.1(eslint@4.19.1)(webpack@4.47.0) + file-loader: 1.1.11(webpack@4.47.0) + glob: 7.2.3 + html-webpack-plugin: 3.2.0(webpack@4.47.0) + lodash.clonedeep: 4.5.0 + lodash.clonedeepwith: 4.5.0 + lodash.get: 4.4.2 + lodash.has: 4.5.2 + lodash.merge: 4.6.2 + lodash.set: 4.3.2 + lodash.uniq: 4.5.0 + md5: 2.3.0 + mini-css-extract-plugin: 0.9.0(webpack@4.47.0) + node-noop: 1.0.0 + node-tool-utils: 1.6.0 + npm-install-webpack-plugin: 4.0.5(webpack@4.47.0) + optimize-css-assets-webpack-plugin: 5.0.8(webpack@4.47.0) + postcss-loader: 3.0.0 + progress-bar-webpack-plugin: 1.12.1(webpack@4.47.0) + service-worker-precache-webpack-plugin: 1.3.5 + shelljs: 0.7.8 + style-loader: 0.18.2 + terser-webpack-plugin: 1.4.6(webpack@4.47.0) + thread-loader: 1.2.0(webpack@4.47.0) + uglifyjs-webpack-plugin: 2.2.0(webpack@4.47.0) + url-loader: 0.5.9(file-loader@1.1.11(webpack@4.47.0)) + vconsole-webpack-plugin: 1.8.0 + webpack-asset-file-plugin: 1.0.2 + webpack-filter-warnings-plugin: 1.2.1(webpack@4.47.0) + webpack-hot-middleware: 2.26.1 + webpack-manifest-resource-plugin: 4.2.7 + webpack-node-externals: 1.7.2 + webpack-tool: 4.5.4 + transitivePeerDependencies: + - debug + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + - typescript + - webpack + - webpack-cli + - webpack-command + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.0 + + ee-first@1.1.1: {} + + egg-bin@4.20.0(@types/node@25.9.1): + dependencies: + c8: 7.14.0 + chalk: 4.1.2 + co-mocha: 1.2.2(mocha@6.2.3) + common-bin: 2.9.2 + debug: 4.4.3 + detect-port: 1.6.1 + egg-ts-helper: 1.35.2(@types/node@25.9.1) + egg-utils: 2.5.0 + espower-source: 2.3.0 + globby: 9.2.0 + inspector-proxy: 1.2.3 + intelli-espower-loader: 1.1.0 + jest-changed-files: 25.5.0 + minimatch: 3.1.5 + mocha: 6.2.3 + mz-modules: 2.1.0 + nyc: 13.3.0 + power-assert: 1.6.1 + semver: 7.8.0 + source-map-support: 0.5.21 + test-exclude: 5.2.3 + ts-node: 7.0.1 + ypkgfiles: 1.6.0 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - proxy-agent + - supports-color + + egg-cluster@1.27.1: + dependencies: + await-event: 2.1.0 + cfork: 1.11.0 + cluster-reload: 1.1.0 + co: 4.6.0 + debug: 4.4.3 + depd: 2.0.0 + detect-port: 1.6.1 + egg-logger: 2.9.1 + egg-utils: 2.5.0 + get-ready: 2.0.1 + graceful-process: 1.3.0 + is-type-of: 1.4.0 + mz-modules: 2.1.0 + ps-tree: 1.2.0 + semver: 5.7.2 + sendmessage: 1.1.0 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + + egg-cookies@2.10.1: + dependencies: + scmp: 2.1.0 + should-send-same-site-none: 2.0.5 + utility: 1.18.0 + + egg-core@4.31.0: + dependencies: + '@eggjs/router': 2.2.0 + '@types/depd': 1.1.37 + '@types/koa': 2.15.2 + co: 4.6.0 + debug: 4.4.3 + depd: 2.0.0 + egg-logger: 2.9.1 + egg-path-matching: 1.2.0 + extend2: 1.0.1 + gals: 1.0.2 + get-ready: 2.0.1 + globby: 10.0.2 + is-type-of: 1.4.0 + koa: 2.16.4 + koa-convert: 1.2.0 + node-homedir: 1.1.1 + ready-callback: 2.1.0 + tsconfig-paths: 4.2.0 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + + egg-cors@2.2.4: + dependencies: + '@koa/cors': 3.4.3 + + egg-development@2.7.0: + dependencies: + debounce: 1.2.1 + multimatch: 2.1.0 + mz: 2.7.0 + mz-modules: 2.1.0 + utility: 1.18.0 + + egg-errors@2.3.2: {} + + egg-i18n@2.1.1: + dependencies: + debug: 3.2.7 + koa-locales: 1.12.0 + transitivePeerDependencies: + - supports-color + + egg-jsonp@2.0.0: + dependencies: + is-type-of: 1.4.0 + jsonp-body: 1.1.0 + + egg-logger@1.8.0: + dependencies: + chalk: 1.1.3 + circular-json: 0.5.9 + debug: 2.6.9 + depd: 1.1.2 + iconv-lite: 0.4.24 + mkdirp: 0.5.6 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + + egg-logger@2.9.1: + dependencies: + chalk: 2.4.2 + circular-json-for-egg: 1.0.0 + debug: 2.6.9 + depd: 2.0.0 + egg-errors: 2.3.2 + iconv-lite: 0.4.24 + mkdirp: 0.5.6 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + + egg-logger@3.6.1: + dependencies: + chalk: 4.1.2 + circular-json-for-egg: 1.0.0 + depd: 2.0.0 + egg-errors: 2.3.2 + iconv-lite: 0.6.3 + utility: 2.5.0 + + egg-logrotator@3.2.0: + dependencies: + moment: 2.30.1 + mz: 2.7.0 + + egg-multipart@2.13.1: + dependencies: + co-busboy: 1.5.0 + egg-path-matching: 1.2.0 + humanize-bytes: 1.0.1 + moment: 2.30.1 + mz: 2.7.0 + mz-modules: 2.1.0 + stream-wormhole: 1.1.0 + uuid: 8.3.2 + + egg-onerror@2.4.0: + dependencies: + cookie: 0.7.2 + koa-onerror: 4.2.0 + mustache: 2.3.2 + stack-trace: 0.0.10 + + egg-path-matching@1.2.0: + dependencies: + path-to-regexp: 1.9.0 + + egg-schedule@3.7.0: + dependencies: + cron-parser: 2.18.0 + humanize-ms: 1.2.1 + is-type-of: 1.4.0 + safe-timers: 1.1.0 + utility: 1.18.0 + + egg-scripts@2.17.0: + dependencies: + await-event: 2.1.0 + common-bin: 2.9.2 + debug: 4.4.3 + egg-utils: 2.5.0 + moment: 2.30.1 + mz: 2.7.0 + mz-modules: 2.1.0 + node-homedir: 1.1.1 + runscript: 1.6.0 + source-map-support: 0.5.21 + zlogger: 1.1.0 + transitivePeerDependencies: + - supports-color + + egg-security@2.11.0: + dependencies: + csrf: 3.1.0 + debug: 4.4.3 + delegates: 1.0.0 + egg-path-matching: 1.2.0 + escape-html: 1.0.3 + extend: 3.0.2 + ip: 1.1.9 + koa-compose: 4.1.0 + matcher: 1.1.1 + methods: 1.1.2 + nanoid: 3.3.12 + platform: 1.3.6 + statuses: 1.5.0 + type-is: 1.6.18 + xss: 1.0.15 + transitivePeerDependencies: + - supports-color + + egg-sequelize@4.3.1: + dependencies: + '@types/sequelize': 4.28.20 + mz-modules: 2.1.0 + sequelize: 4.44.4 + transitivePeerDependencies: + - supports-color + + egg-session@3.3.0: + dependencies: + koa-session: 6.4.0 + transitivePeerDependencies: + - supports-color + + egg-socket.io@4.1.6: + dependencies: + debug: 4.4.3 + delegates: 1.0.0 + is-type-of: 1.4.0 + koa-compose: 4.1.0 + socket.io: 2.5.1 + socket.io-redis: 5.4.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + egg-ssh@1.0.5: + dependencies: + '@types/node-ssh': 7.0.6 + node-ssh: 10.0.2 + ssh2: 0.8.9 + + egg-static@2.3.1: + dependencies: + is-type-of: 1.4.0 + koa-compose: 4.1.0 + koa-range: 0.3.0 + koa-static-cache: 5.1.4 + ylru: 1.4.0 + transitivePeerDependencies: + - supports-color + + egg-ts-helper@1.35.2(@types/node@25.9.1): + dependencies: + chalk: 2.4.2 + chokidar: 3.6.0 + commander: 2.20.3 + dot-prop: 4.2.1 + enquirer: 2.4.1 + globby: 11.1.0 + json5: 2.2.3 + ts-node: 10.9.2(@types/node@25.9.1)(typescript@4.7.4) + tslib: 2.8.1 + typescript: 4.7.4 + yn: 3.1.1 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + + egg-utils@2.5.0: + dependencies: + mkdirp: 0.5.6 + utility: 1.18.0 + + egg-validate@1.1.2: + dependencies: + parameter: 2.4.0 + + egg-view-react-ssr@2.5.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + serialize-javascript: 2.1.2 + server-side-render-resource: 1.2.0 + + egg-view@2.1.4: + dependencies: + mz: 2.7.0 + + egg-watcher@3.1.1: + dependencies: + camelcase: 5.3.1 + sdk-base: 3.6.0 + wt: 1.2.0 + transitivePeerDependencies: + - supports-color + + egg-webpack-react@2.0.3: + dependencies: + co: 4.6.0 + + egg-webpack@4.5.5: + dependencies: + co-request: 0.2.1 + iconv-lite: 0.2.11 + koa-convert: 1.2.0 + mkdirp: 0.5.6 + webpack-tool: 4.5.4 + transitivePeerDependencies: + - debug + - supports-color + - webpack-cli + - webpack-command + + egg@2.37.0: + dependencies: + '@types/accepts': 1.3.7 + '@types/koa': 2.15.2 + '@types/koa-router': 7.4.9 + accepts: 1.3.8 + agentkeepalive: 4.6.0 + cache-content-type: 1.0.1 + circular-json-for-egg: 1.0.0 + cluster-client: 3.7.0 + debug: 4.4.3 + delegates: 1.0.0 + egg-cluster: 1.27.1 + egg-cookies: 2.10.1 + egg-core: 4.31.0 + egg-development: 2.7.0 + egg-errors: 2.3.2 + egg-i18n: 2.1.1 + egg-jsonp: 2.0.0 + egg-logger: 2.9.1 + egg-logrotator: 3.2.0 + egg-multipart: 2.13.1 + egg-onerror: 2.4.0 + egg-schedule: 3.7.0 + egg-security: 2.11.0 + egg-session: 3.3.0 + egg-static: 2.3.1 + egg-view: 2.1.4 + egg-watcher: 3.1.1 + extend2: 1.0.1 + graceful: 1.1.0 + humanize-ms: 1.2.1 + is-type-of: 1.4.0 + koa-bodyparser: 4.4.1 + koa-is-json: 1.0.0 + koa-override: 3.0.0 + ms: 2.1.3 + mz: 2.7.0 + on-finished: 2.4.1 + semver: 7.8.0 + sendmessage: 1.1.0 + urllib: 2.44.0 + utility: 1.18.0 + ylru: 1.4.0 + transitivePeerDependencies: + - proxy-agent + - supports-color + + ejs@2.7.4: {} + + electron-to-chromium@1.5.360: {} + + elliptic@6.6.1: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + emoji-regex@7.0.3: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojis-list@2.1.0: {} + + emojis-list@3.0.0: {} + + empower-assert@1.1.0: + dependencies: + estraverse: 4.3.0 + + empower-core@1.2.0: + dependencies: + call-signature: 0.0.2 + core-js: 2.6.12 + + empower@1.3.1: + dependencies: + core-js: 2.6.12 + empower-core: 1.2.0 + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@1.8.1: + dependencies: + component-emitter: 1.2.1 + component-inherit: 0.0.3 + debug: 2.3.3 + engine.io-parser: 1.3.1 + has-cors: 1.1.0 + indexof: 0.0.1 + parsejson: 0.0.3 + parseqs: 0.0.5 + parseuri: 0.0.5 + ws: 1.1.1 + xmlhttprequest-ssl: 1.5.3 + yeast: 0.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-client@3.5.6: + dependencies: + component-emitter: 1.3.1 + component-inherit: 0.0.3 + debug: 3.1.0 + engine.io-parser: 2.2.1 + has-cors: 1.1.0 + indexof: 0.0.1 + parseqs: 0.0.6 + parseuri: 0.0.6 + ws: 7.5.10 + xmlhttprequest-ssl: 1.6.3 + yeast: 0.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@1.3.1: + dependencies: + after: 0.8.1 + arraybuffer.slice: 0.0.6 + base64-arraybuffer: 0.1.5 + blob: 0.0.4 + has-binary: 0.1.6 + wtf-8: 1.0.0 + + engine.io-parser@2.2.1: + dependencies: + after: 0.8.2 + arraybuffer.slice: 0.0.7 + base64-arraybuffer: 0.1.4 + blob: 0.0.5 + has-binary2: 1.0.3 + + engine.io-parser@5.2.3: {} + + engine.io@3.6.2: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + debug: 4.1.1 + engine.io-parser: 2.2.1 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io@6.6.8: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 25.9.1 + '@types/ws': 8.18.1 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.6 + debug: 4.4.3 + engine.io-parser: 5.2.3 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@3.4.1: + dependencies: + graceful-fs: 4.2.11 + memory-fs: 0.4.1 + object-assign: 4.1.1 + tapable: 0.2.9 + + enhanced-resolve@4.5.0: + dependencies: + graceful-fs: 4.2.11 + memory-fs: 0.5.0 + tapable: 1.1.3 + + enquire.js@2.1.6: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + env-paths@2.2.1: + optional: true + + errno@0.1.8: + dependencies: + prr: 1.0.1 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-inject@1.0.0: {} + + errorhandler@1.4.3: + dependencies: + accepts: 1.3.8 + escape-html: 1.0.3 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-array-method-boxes-properly@1.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.2: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es-toolkit@1.46.1: + optional: true + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-map@0.1.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-set: 0.1.6 + es6-symbol: 3.1.4 + event-emitter: 0.3.5 + + es6-promise@4.2.8: {} + + es6-set@0.1.6: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + event-emitter: 0.3.5 + type: 2.7.3 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + es6-weak-map@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + + escalade@3.2.0: {} + + escallmatch@1.5.0: + dependencies: + call-matcher: 1.1.0 + esprima: 2.7.3 + + escape-goat@2.1.1: {} + + escape-html@1.0.2: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escodegen@1.14.3: + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + escope@3.6.0: + dependencies: + es6-map: 0.1.5 + es6-weak-map: 2.0.3 + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-config-egg@5.1.1(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0): + dependencies: + babel-eslint: 7.2.3 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.22.0) + eslint-plugin-react: 7.37.5(eslint@8.22.0) + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-config-egg@7.5.1(eslint@4.19.1)(typescript@4.7.4): + dependencies: + '@typescript-eslint/eslint-plugin': 2.34.0(@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4))(eslint@4.19.1)(typescript@4.7.4) + '@typescript-eslint/parser': 2.34.0(eslint@4.19.1)(typescript@4.7.4) + babel-eslint: 8.2.6 + eslint-plugin-eggache: 1.0.0 + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4))(eslint@4.19.1) + eslint-plugin-jsdoc: 4.8.4(eslint@4.19.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@4.19.1) + eslint-plugin-react: 7.37.5(eslint@4.19.1) + transitivePeerDependencies: + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + - typescript + + eslint-config-prettier@8.5.0(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + + eslint-config-standard@17.0.0(eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0))(eslint-plugin-n@15.2.3(eslint@8.22.0))(eslint-plugin-promise@6.0.0(eslint@8.22.0))(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0) + eslint-plugin-n: 15.2.3(eslint@8.22.0) + eslint-plugin-promise: 6.0.0(eslint@8.22.0) + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-loader@2.2.1(eslint@4.19.1)(webpack@4.47.0): + dependencies: + eslint: 4.19.1 + loader-fs-cache: 1.0.3 + loader-utils: 1.4.2 + object-assign: 4.1.1 + object-hash: 1.3.1 + rimraf: 2.7.1 + webpack: 4.47.0 + + eslint-module-utils@2.12.1(@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4))(eslint-import-resolver-node@0.3.10)(eslint@4.19.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 2.34.0(eslint@4.19.1)(typescript@4.7.4) + eslint: 4.19.1 + eslint-import-resolver-node: 0.3.10 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.10)(eslint@8.22.0): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.10 + transitivePeerDependencies: + - supports-color + + eslint-plugin-dt-react@0.0.6: + dependencies: + eslint: 8.22.0 + object.hasown: 1.1.4 + semver: 7.8.0 + transitivePeerDependencies: + - supports-color + + eslint-plugin-eggache@1.0.0: {} + + eslint-plugin-es@4.1.0(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + + eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0): + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + debug: 2.6.9 + doctrine: 2.1.0 + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.10)(eslint@8.22.0) + has: 1.0.4 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.values: 1.2.1 + resolve: 1.22.12 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4))(eslint@4.19.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 4.19.1 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@2.34.0(eslint@4.19.1)(typescript@4.7.4))(eslint-import-resolver-node@0.3.10)(eslint@4.19.1) + hasown: 2.0.3 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 2.34.0(eslint@4.19.1)(typescript@4.7.4) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint-import-resolver-node@0.3.10)(eslint@8.22.0) + hasown: 2.0.3 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jest@26.5.3(@typescript-eslint/eslint-plugin@5.30.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4): + dependencies: + '@typescript-eslint/utils': 5.62.0(eslint@8.22.0)(typescript@4.7.4) + eslint: 8.22.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 5.30.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-jsdoc@4.8.4(eslint@4.19.1): + dependencies: + comment-parser: 0.5.5 + eslint: 4.19.1 + jsdoctypeparser: 3.1.0 + lodash: 4.18.1 + + eslint-plugin-jsx-a11y@6.10.2(eslint@4.19.1): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.4 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 4.19.1 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-jsx-a11y@6.10.2(eslint@8.22.0): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.11.4 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.22.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-jsx-a11y@6.6.0(eslint@8.22.0): + dependencies: + '@babel/runtime': 7.29.2 + aria-query: 4.2.2 + array-includes: 3.1.9 + ast-types-flow: 0.0.7 + axe-core: 4.11.4 + axobject-query: 2.2.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.22.0 + has: 1.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + semver: 6.3.1 + + eslint-plugin-n@15.2.3(eslint@8.22.0): + dependencies: + builtins: 5.1.0 + eslint: 8.22.0 + eslint-plugin-es: 4.1.0(eslint@8.22.0) + eslint-utils: 3.0.0(eslint@8.22.0) + ignore: 5.3.2 + is-core-module: 2.16.2 + minimatch: 3.1.5 + resolve: 1.22.12 + semver: 7.8.0 + + eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.5.0(eslint@8.22.0))(eslint@8.22.0)(prettier@2.7.1): + dependencies: + eslint: 8.22.0 + prettier: 2.7.1 + prettier-linter-helpers: 1.0.1 + optionalDependencies: + eslint-config-prettier: 8.5.0(eslint@8.22.0) + + eslint-plugin-promise@6.0.0(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + + eslint-plugin-react-hooks@4.6.0(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + + eslint-plugin-react@7.30.1(eslint@8.22.0): + dependencies: + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + doctrine: 2.1.0 + eslint: 8.22.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.hasown: 1.1.4 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + + eslint-plugin-react@7.37.5(eslint@4.19.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.2 + eslint: 4.19.1 + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-plugin-react@7.37.5(eslint@8.22.0): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.2 + eslint: 8.22.0 + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-plugin-simple-import-sort@10.0.0(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + + eslint-plugin-sort-requires@2.1.0: {} + + eslint-scope@3.7.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@3.7.3: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@4.0.3: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-utils@2.1.0: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-utils@3.0.0(eslint@8.22.0): + dependencies: + eslint: 8.22.0 + eslint-visitor-keys: 2.1.0 + + eslint-visitor-keys@1.3.0: {} + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint@4.19.1: + dependencies: + ajv: 5.5.2 + babel-code-frame: 6.26.0 + chalk: 2.4.2 + concat-stream: 1.6.2 + cross-spawn: 5.1.0 + debug: 3.2.7 + doctrine: 2.1.0 + eslint-scope: 3.7.3 + eslint-visitor-keys: 1.3.0 + espree: 3.5.4 + esquery: 1.7.0 + esutils: 2.0.3 + file-entry-cache: 2.0.0 + functional-red-black-tree: 1.0.1 + glob: 7.2.3 + globals: 11.12.0 + ignore: 3.3.10 + imurmurhash: 0.1.4 + inquirer: 3.3.0 + is-resolvable: 1.1.0 + js-yaml: 3.14.2 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.3.0 + lodash: 4.18.1 + minimatch: 3.1.5 + mkdirp: 0.5.6 + natural-compare: 1.4.0 + optionator: 0.8.3 + path-is-inside: 1.0.2 + pluralize: 7.0.0 + progress: 2.0.3 + regexpp: 1.1.0 + require-uncached: 1.0.3 + semver: 5.7.2 + strip-ansi: 4.0.0 + strip-json-comments: 2.0.1 + table: 4.0.2 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + eslint@8.22.0: + dependencies: + '@eslint/eslintrc': 1.4.1 + '@humanwhocodes/config-array': 0.10.7 + '@humanwhocodes/gitignore-to-minimatch': 1.0.2 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-utils: 3.0.0(eslint@8.22.0) + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + functional-red-black-tree: 1.0.1 + glob-parent: 6.0.2 + globals: 13.24.0 + globby: 11.1.0 + grapheme-splitter: 1.0.4 + ignore: 5.3.2 + import-fresh: 3.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + v8-compile-cache: 2.4.0 + transitivePeerDependencies: + - supports-color + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + espower-loader@1.2.2: + dependencies: + convert-source-map: 1.9.0 + espower-source: 2.3.0 + minimatch: 3.1.5 + source-map-support: 0.4.18 + xtend: 4.0.2 + + espower-location-detector@1.0.0: + dependencies: + is-url: 1.2.4 + path-is-absolute: 1.0.1 + source-map: 0.5.7 + xtend: 4.0.2 + + espower-source@2.3.0: + dependencies: + acorn: 5.7.4 + acorn-es7-plugin: 1.1.7 + convert-source-map: 1.9.0 + empower-assert: 1.1.0 + escodegen: 1.14.3 + espower: 2.1.2 + estraverse: 4.3.0 + merge-estraverse-visitors: 1.0.0 + multi-stage-sourcemap: 0.2.1 + path-is-absolute: 1.0.1 + xtend: 4.0.2 + + espower@2.1.2: + dependencies: + array-find: 1.0.0 + escallmatch: 1.5.0 + escodegen: 1.14.3 + escope: 3.6.0 + espower-location-detector: 1.0.0 + espurify: 1.8.1 + estraverse: 4.3.0 + source-map: 0.5.7 + type-name: 2.0.2 + + espree@3.5.4: + dependencies: + acorn: 5.7.4 + acorn-jsx: 3.0.1 + + espree@9.6.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 + + esprima@2.7.3: {} + + esprima@4.0.1: {} + + espurify@1.8.1: + dependencies: + core-js: 2.6.12 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.7.0: {} + + etag@1.8.1: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + eventemitter3@4.0.7: {} + + eventlistener@0.0.1: {} + + events@3.3.0: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + + evp_bytestokey@1.0.3: + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + + execa@0.7.0: + dependencies: + cross-spawn: 5.1.0 + get-stream: 3.0.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + execa@3.4.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + p-finally: 2.0.1 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express-session@1.11.3: + dependencies: + cookie: 0.1.3 + cookie-signature: 1.0.6 + crc: 3.3.0 + debug: 2.2.0 + depd: 1.0.1 + on-headers: 1.0.2 + parseurl: 1.3.3 + uid-safe: 2.0.0 + utils-merge: 1.0.0 + transitivePeerDependencies: + - supports-color + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.5 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + ext@1.7.0: + dependencies: + type: 2.7.3 + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extend2@1.0.1: {} + + extend@3.0.2: {} + + external-editor@2.2.0: + dependencies: + chardet: 0.4.2 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + fast-deep-equal@1.1.0: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@2.2.7: + dependencies: + '@mrmlnc/readdir-enhanced': 2.2.1 + '@nodelib/fs.stat': 1.1.3 + glob-parent: 3.1.0 + is-glob: 4.0.3 + merge2: 1.4.1 + micromatch: 3.1.10 + transitivePeerDependencies: + - supports-color + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.2: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fault@1.0.4: + dependencies: + format: 0.2.2 + + fbjs@0.8.18: + dependencies: + core-js: 1.2.7 + isomorphic-fetch: 2.2.1 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 0.7.41 + + fd-slicer2@1.2.0: + dependencies: + pend: 1.2.0 + + figgy-pudding@3.5.2: {} + + figlet@1.11.0: + dependencies: + commander: 14.0.3 + + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@2.0.0: + dependencies: + flat-cache: 1.3.4 + object-assign: 4.1.1 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-loader@1.1.11(webpack@4.47.0): + dependencies: + loader-utils: 1.4.2 + schema-utils: 0.4.7 + webpack: 4.47.0 + + file-loader@3.0.1(webpack@4.47.0): + dependencies: + loader-utils: 1.4.2 + schema-utils: 1.0.0 + webpack: 4.47.0 + + file-saver@1.3.8: {} + + file-uri-to-path@1.0.0: + optional: true + + filesize@3.6.1: {} + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@0.4.0: + dependencies: + debug: 2.2.0 + escape-html: 1.0.2 + on-finished: 2.3.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-cache-dir@0.1.1: + dependencies: + commondir: 1.0.1 + mkdirp: 0.5.6 + pkg-dir: 1.0.0 + + find-cache-dir@1.0.0: + dependencies: + commondir: 1.0.1 + make-dir: 1.3.0 + pkg-dir: 2.0.0 + + find-cache-dir@2.1.0: + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + + find-node-modules@2.1.3: + dependencies: + findup-sync: 4.0.0 + merge: 2.1.1 + + find-root@1.1.0: {} + + find-up@1.1.2: + dependencies: + path-exists: 2.1.0 + pinkie-promise: 2.0.1 + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + findup-sync@4.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + + flat-cache@1.3.4: + dependencies: + circular-json: 0.3.3 + graceful-fs: 4.2.11 + rimraf: 2.6.3 + write: 0.2.1 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat@4.1.1: + dependencies: + is-buffer: 2.0.5 + + flatted@3.4.2: {} + + flush-write-stream@1.1.1: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + flushwritable@1.0.0: {} + + follow-redirects@1.16.0: {} + + follow-redirects@1.5.10: + dependencies: + debug: 3.1.0 + transitivePeerDependencies: + - supports-color + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + for-in@1.0.2: {} + + foreground-child@2.0.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 3.0.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + form-data@3.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + format@0.2.2: {} + + formstream@1.5.2: + dependencies: + destroy: 1.2.0 + mime: 2.6.0 + node-hex: 1.0.1 + pause-stream: 0.0.11 + + forwarded@0.2.0: {} + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fresh@0.3.0: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + from2@2.3.0: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + from@0.1.7: {} + + fs-constants@1.0.0: {} + + fs-extra@0.30.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + klaw: 1.3.1 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + + fs-extra@5.0.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-readdir-recursive@1.1.0: {} + + fs-write-stream-atomic@1.0.10: + dependencies: + graceful-fs: 4.2.11 + iferr: 0.1.5 + imurmurhash: 0.1.4 + readable-stream: 2.3.8 + + fs.realpath@1.0.0: {} + + fsevents@1.2.13: + dependencies: + bindings: 1.5.0 + nan: 2.27.0 + optional: true + + fsevents@2.3.3: + optional: true + + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.3 + is-callable: 1.2.7 + + functional-red-black-tree@1.0.1: {} + + functions-have-names@1.2.3: {} + + gals@1.0.2: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + generator-function@2.0.1: {} + + generic-pool@3.5.0: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@1.0.3: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-pkg-repo@4.2.1: + dependencies: + '@hutson/parse-repository-url': 3.0.2 + hosted-git-info: 4.1.0 + through2: 2.0.5 + yargs: 16.2.0 + + get-port@5.1.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-ready@1.0.0: {} + + get-ready@2.0.1: + dependencies: + is-type-of: 1.4.0 + + get-stdin@4.0.1: {} + + get-stdin@7.0.0: {} + + get-stream@3.0.0: {} + + get-stream@4.1.0: + dependencies: + pump: 3.0.4 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-value@2.0.6: {} + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + git-raw-commits@2.0.11: + dependencies: + dargs: 7.0.0 + lodash: 4.18.1 + meow: 8.1.2 + split2: 3.2.2 + through2: 4.0.2 + + git-remote-origin-url@2.0.0: + dependencies: + gitconfiglocal: 1.0.0 + pify: 2.3.0 + + git-semver-tags@4.1.1: + dependencies: + meow: 8.1.2 + semver: 6.3.1 + + gitconfiglocal@1.0.0: + dependencies: + ini: 1.3.8 + + glob-parent@3.1.0: + dependencies: + is-glob: 3.1.0 + path-dirname: 1.0.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.1.0: {} + + glob-to-regexp@0.3.0: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.1.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@5.0.0: + dependencies: + ini: 6.0.0 + optional: true + + global-dirs@0.1.1: + dependencies: + ini: 1.3.8 + + global-dirs@2.1.0: + dependencies: + ini: 1.3.7 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + global@4.4.0: + dependencies: + min-document: 2.19.2 + process: 0.11.10 + + globals@11.12.0: {} + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@9.18.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@10.0.2: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@7.1.1: + dependencies: + array-union: 1.0.2 + dir-glob: 2.2.2 + glob: 7.2.3 + ignore: 3.3.10 + pify: 3.0.0 + slash: 1.0.0 + + globby@9.2.0: + dependencies: + '@types/glob': 7.2.0 + array-union: 1.0.2 + dir-glob: 2.2.2 + fast-glob: 2.2.7 + glob: 7.2.3 + ignore: 4.0.6 + pify: 4.0.1 + slash: 2.0.0 + transitivePeerDependencies: + - supports-color + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + got@6.7.1: + dependencies: + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + create-error-class: 3.0.2 + duplexer3: 0.1.5 + get-stream: 3.0.0 + is-redirect: 1.0.0 + is-retry-allowed: 1.2.0 + is-stream: 1.1.0 + lowercase-keys: 1.0.1 + safe-buffer: 5.2.1 + timed-out: 4.0.1 + unzip-response: 2.0.1 + url-parse-lax: 1.0.0 + + got@9.6.0: + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + + graceful-fs@4.2.11: {} + + graceful-process@1.3.0: + dependencies: + is-type-of: 1.4.0 + once: 1.4.0 + + graceful@1.1.0: + dependencies: + humanize-ms: 1.2.1 + ps-tree: 1.2.0 + + grapheme-splitter@1.0.4: {} + + growl@1.10.5: {} + + gud@1.0.0: {} + + gzip-size@5.1.1: + dependencies: + duplexer: 0.1.2 + pify: 4.0.1 + + hammerjs@2.0.8: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.15.0 + har-schema: 2.0.0 + + hard-rejection@2.1.0: {} + + has-ansi@2.0.0: + dependencies: + ansi-regex: 2.1.1 + + has-bigints@1.1.0: {} + + has-binary2@1.0.3: + dependencies: + isarray: 2.0.1 + + has-binary@0.1.6: + dependencies: + isarray: 0.0.1 + + has-binary@0.1.7: + dependencies: + isarray: 0.0.1 + + has-cors@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + has-yarn@2.1.0: {} + + has@1.0.4: {} + + hash-base@3.0.5: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + hash-base@3.1.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + hash.js@1.1.7: + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-parse-selector@2.2.5: {} + + hastscript@6.0.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 1.0.8 + hast-util-parse-selector: 2.2.5 + property-information: 5.6.0 + space-separated-tokens: 1.1.5 + + he@1.2.0: {} + + header-case@1.0.1: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + hex-color-regex@1.1.0: {} + + highlight.js@10.7.3: {} + + highlightjs-vue@1.0.0: {} + + history@4.10.1: + dependencies: + '@babel/runtime': 7.29.2 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + + hmac-drbg@1.0.1: + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + + hoist-non-react-statics@2.5.5: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + home-or-tmp@2.0.0: + dependencies: + os-homedir: 1.0.2 + os-tmpdir: 1.0.2 + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hono@4.12.21: {} + + hoopy@0.1.4: {} + + hosted-git-info@2.8.9: {} + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hsl-regex@1.0.0: {} + + hsla-regex@1.0.0: {} + + html-encoding-sniffer@1.0.2: + dependencies: + whatwg-encoding: 1.0.5 + + html-encoding-sniffer@2.0.1: + dependencies: + whatwg-encoding: 1.0.5 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier@3.5.21: + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.17.1 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.4.10 + + html-tags@3.3.1: {} + + html-webpack-plugin@3.2.0(webpack@4.47.0): + dependencies: + html-minifier: 3.5.21 + loader-utils: 0.2.17 + lodash: 4.18.1 + pretty-error: 2.1.2 + tapable: 1.1.3 + toposort: 1.0.7 + util.promisify: 1.0.0 + webpack: 4.47.0 + + html2canvas@0.5.0-beta4: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + http-assert@1.5.0: + dependencies: + deep-equal: 1.0.1 + http-errors: 1.8.1 + + http-cache-semantics@4.2.0: {} + + http-errors@1.3.1: + dependencies: + inherits: 2.0.4 + statuses: 1.5.0 + + http-errors@1.8.1: + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 1.5.0 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http-proxy-middleware@0.20.0: + dependencies: + http-proxy: 1.18.1 + is-glob: 4.0.3 + lodash: 4.18.1 + micromatch: 4.0.8 + transitivePeerDependencies: + - debug + + http-proxy-middleware@2.0.6: + dependencies: + '@types/http-proxy': 1.17.17 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + + https-browserify@1.0.0: {} + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@1.1.1: {} + + humanize-bytes@1.0.1: + dependencies: + bytes: 2.2.0 + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + husky@3.1.0: + dependencies: + chalk: 2.4.2 + ci-info: 2.0.0 + cosmiconfig: 5.2.1 + execa: 1.0.0 + get-stdin: 7.0.0 + opencollective-postinstall: 2.0.3 + pkg-dir: 4.2.0 + please-upgrade-node: 3.2.0 + read-pkg: 5.2.0 + run-node: 1.0.0 + slash: 3.0.0 + + iconv-lite@0.2.11: {} + + iconv-lite@0.4.11: {} + + iconv-lite@0.4.13: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.5.2: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@4.1.1: + dependencies: + postcss: 7.0.39 + + ieee754@1.2.1: {} + + iferr@0.1.5: {} + + ignore@3.3.10: {} + + ignore@4.0.6: {} + + ignore@5.3.2: {} + + image-size@0.5.5: + optional: true + + immutable@3.7.6: {} + + immutable@5.1.5: {} + + import-cwd@2.1.0: + dependencies: + import-from: 2.1.0 + + import-fresh@2.0.0: + dependencies: + caller-path: 2.0.0 + resolve-from: 3.0.0 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-from@2.1.0: + dependencies: + resolve-from: 3.0.0 + + import-lazy@2.1.0: {} + + import-lazy@4.0.0: {} + + imurmurhash@0.1.4: {} + + indent-string@2.1.0: + dependencies: + repeating: 2.0.1 + + indent-string@3.2.0: {} + + indent-string@4.0.0: {} + + indexes-of@1.0.1: {} + + indexof@0.0.1: {} + + infer-owner@1.0.4: {} + + inflation@2.1.0: {} + + inflection@1.12.0: {} + + inflection@1.13.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.7: {} + + ini@1.3.8: {} + + ini@6.0.0: + optional: true + + inline-style-parser@0.1.1: {} + + inquirer@3.3.0: + dependencies: + ansi-escapes: 3.2.0 + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-width: 2.2.1 + external-editor: 2.2.0 + figures: 2.0.0 + lodash: 4.18.1 + mute-stream: 0.0.7 + run-async: 2.4.1 + rx-lite: 4.0.8 + rx-lite-aggregates: 4.0.8 + string-width: 2.1.1 + strip-ansi: 4.0.0 + through: 2.3.8 + + inquirer@7.3.3: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.18.1 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + + inquirer@8.2.5: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.18.1 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + insert-css@2.0.0: {} + + inspector-proxy@1.2.3: + dependencies: + cfork: 1.11.0 + debug: 3.2.7 + tcp-proxy.js: 1.5.0 + urllib: 2.44.0 + transitivePeerDependencies: + - proxy-agent + - supports-color + + intelli-espower-loader@1.1.0: + dependencies: + espower-loader: 1.2.2 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + + interpret@1.4.0: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + invert-kv@1.0.0: {} + + ip-address@10.2.0: {} + + ip-regex@2.1.0: {} + + ip@1.1.9: {} + + ipaddr.js@1.9.1: {} + + is-absolute-url@2.1.0: {} + + is-accessor-descriptor@1.0.2: + dependencies: + hasown: 2.0.3 + + is-alphabetical@1.0.4: {} + + is-alphanumerical@1.0.4: + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@1.0.1: + dependencies: + binary-extensions: 1.13.1 + optional: true + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-bluebird@1.0.2: {} + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-buffer@2.0.5: {} + + is-callable@1.2.7: {} + + is-ci@1.2.1: + dependencies: + ci-info: 1.6.0 + + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + + is-class-hotfix@0.0.6: {} + + is-color-stop@1.1.0: + dependencies: + css-color-names: 0.0.4 + hex-color-regex: 1.1.0 + hsl-regex: 1.0.0 + hsla-regex: 1.0.0 + rgb-regex: 1.0.1 + rgba-regex: 1.0.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@1.0.4: {} + + is-descriptor@0.1.8: + dependencies: + is-accessor-descriptor: 1.0.2 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.4: + dependencies: + is-accessor-descriptor: 1.0.2 + is-data-descriptor: 1.0.1 + + is-directory@0.3.1: {} + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-finite@1.1.0: {} + + is-fullwidth-code-point@1.0.0: + dependencies: + number-is-nan: 1.0.1 + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-generator@1.0.3: {} + + is-glob@3.1.0: + dependencies: + is-extglob: 2.1.1 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@1.0.4: {} + + is-installed-globally@0.1.0: + dependencies: + global-dirs: 0.1.1 + is-path-inside: 1.0.1 + + is-installed-globally@0.3.2: + dependencies: + global-dirs: 2.1.0 + is-path-inside: 3.0.3 + + is-interactive@1.0.0: {} + + is-keyword-js@1.0.3: {} + + is-lower-case@1.1.3: + dependencies: + lower-case: 1.1.4 + + is-map@2.0.3: {} + + is-mobile@2.2.2: {} + + is-nan@1.3.2: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + is-negative-zero@2.0.3: {} + + is-npm@1.0.0: {} + + is-npm@4.0.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-number@7.0.0: {} + + is-obj@1.0.1: {} + + is-obj@2.0.0: {} + + is-path-inside@1.0.1: + dependencies: + path-is-inside: 1.0.2 + + is-path-inside@3.0.3: {} + + is-plain-obj@1.1.0: {} + + is-plain-obj@2.1.0: {} + + is-plain-obj@3.0.0: {} + + is-plain-obj@4.1.0: + optional: true + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-promise@2.2.2: {} + + is-promise@4.0.0: {} + + is-property@1.0.2: {} + + is-redirect@1.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-resolvable@1.1.0: {} + + is-retry-allowed@1.2.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@1.1.0: {} + + is-stream@2.0.1: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-text-path@1.0.1: + dependencies: + text-extensions: 1.9.0 + + is-type-of@1.4.0: + dependencies: + core-util-is: 1.0.3 + is-class-hotfix: 0.0.6 + isstream: 0.1.2 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-upper-case@1.1.2: + dependencies: + upper-case: 1.1.3 + + is-url@1.2.4: {} + + is-utf8@0.2.1: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-windows@1.0.2: {} + + is-wsl@1.1.0: {} + + is-yarn-global@0.3.0: {} + + isarray@0.0.1: {} + + isarray@1.0.0: {} + + isarray@2.0.1: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + isomorphic-fetch@2.2.1: + dependencies: + node-fetch: 1.7.3 + whatwg-fetch: 3.6.20 + + isomorphic-style-loader@4.0.0: + dependencies: + babel-runtime: 6.26.0 + hoist-non-react-statics: 2.5.5 + loader-utils: 1.4.2 + prop-types: 15.8.1 + + isstream@0.1.2: {} + + istanbul-lib-coverage@2.0.5: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@3.3.0: + dependencies: + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + istanbul-lib-coverage: 2.0.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@25.5.0: + dependencies: + '@jest/types': 25.5.0 + execa: 3.4.0 + throat: 5.0.0 + + jiti@2.6.1: + optional: true + + jose@6.2.3: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.7 + nopt: 7.2.1 + + js-cookie@2.2.1: {} + + js-cookie@3.0.7: {} + + js-tokens@3.0.2: {} + + js-tokens@4.0.0: {} + + js-yaml@3.13.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsbn@0.1.1: {} + + jsdoctypeparser@3.1.0: {} + + jsdom@15.2.1: + dependencies: + abab: 2.0.6 + acorn: 7.4.1 + acorn-globals: 4.3.4 + array-equal: 1.0.2 + cssom: 0.4.4 + cssstyle: 2.3.0 + data-urls: 1.1.0 + domexception: 1.0.1 + escodegen: 1.14.3 + html-encoding-sniffer: 1.0.2 + nwsapi: 2.2.23 + parse5: 5.1.0 + pn: 1.1.0 + request: 2.88.2 + request-promise-native: 1.0.9(request@2.88.2) + saxes: 3.1.11 + symbol-tree: 3.2.4 + tough-cookie: 3.0.1 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 1.1.2 + webidl-conversions: 4.0.2 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 7.1.0 + ws: 7.5.10 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + jsdom@16.7.0: + dependencies: + abab: 2.0.6 + acorn: 8.16.0 + acorn-globals: 6.0.0 + cssom: 0.4.4 + cssstyle: 2.3.0 + data-urls: 2.0.0 + decimal.js: 10.6.0 + domexception: 2.0.1 + escodegen: 2.1.0 + form-data: 3.0.4 + html-encoding-sniffer: 2.0.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 2.0.0 + webidl-conversions: 6.1.0 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + ws: 7.5.10 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@0.5.0: {} + + jsesc@1.3.0: {} + + jsesc@2.5.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.0: {} + + json-buffer@3.0.1: {} + + json-parse-better-errors@1.0.2: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + json2mq@0.2.0: + dependencies: + string-convert: 0.2.1 + + json3@3.3.2: {} + + json5@0.5.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonfile@2.4.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonp-body@1.1.0: {} + + jsonparse@1.3.1: {} + + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + kcors@1.3.3: + dependencies: + copy-to: 2.0.1 + + keygrip@1.1.0: + dependencies: + tsscmp: 1.0.6 + + keyv@3.1.0: + dependencies: + json-buffer: 3.0.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@6.0.3: {} + + klaw@1.3.1: + optionalDependencies: + graceful-fs: 4.2.11 + + klona@2.0.6: {} + + known-css-properties@0.25.0: {} + + ko-lint-config@2.2.22(typescript@4.7.4): + dependencies: + '@typescript-eslint/eslint-plugin': 5.30.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4) + '@typescript-eslint/parser': 5.30.0(eslint@8.22.0)(typescript@4.7.4) + eslint: 8.22.0 + eslint-config-prettier: 8.5.0(eslint@8.22.0) + eslint-config-standard: 17.0.0(eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0))(eslint-plugin-n@15.2.3(eslint@8.22.0))(eslint-plugin-promise@6.0.0(eslint@8.22.0))(eslint@8.22.0) + eslint-plugin-dt-react: 0.0.6 + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0) + eslint-plugin-jest: 26.5.3(@typescript-eslint/eslint-plugin@5.30.0(@typescript-eslint/parser@5.30.0(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4))(eslint@8.22.0)(typescript@4.7.4) + eslint-plugin-jsx-a11y: 6.6.0(eslint@8.22.0) + eslint-plugin-n: 15.2.3(eslint@8.22.0) + eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.5.0(eslint@8.22.0))(eslint@8.22.0)(prettier@2.7.1) + eslint-plugin-promise: 6.0.0(eslint@8.22.0) + eslint-plugin-react: 7.30.1(eslint@8.22.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.22.0) + eslint-plugin-simple-import-sort: 10.0.0(eslint@8.22.0) + eslint-plugin-sort-requires: 2.1.0 + postcss: 8.4.14 + postcss-less: 6.0.0(postcss@8.4.14) + postcss-scss: 4.0.4(postcss@8.4.14) + prettier: 2.7.1 + stylelint: 14.11.0 + stylelint-config-standard: 25.0.0(stylelint@14.11.0) + stylelint-order: 5.0.0(stylelint@14.11.0) + stylelint-scss: 4.3.0(stylelint@14.11.0) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + - typescript + + ko-sleep@1.1.4: + dependencies: + ms: 2.1.3 + + koa-bodyparser@4.4.1: + dependencies: + co-body: 6.2.0 + copy-to: 2.0.1 + type-is: 1.6.18 + + koa-compose@2.5.1: {} + + koa-compose@3.2.1: + dependencies: + any-promise: 1.3.0 + + koa-compose@4.1.0: {} + + koa-connect@1.0.0: + dependencies: + connect: 2.30.2 + transitivePeerDependencies: + - supports-color + + koa-connect@2.1.1: {} + + koa-convert@1.2.0: + dependencies: + co: 4.6.0 + koa-compose: 3.2.1 + + koa-convert@2.0.0: + dependencies: + co: 4.6.0 + koa-compose: 4.1.0 + + koa-is-json@1.0.0: {} + + koa-locales@1.12.0: + dependencies: + debug: 2.6.9 + humanize-ms: 1.2.1 + ini: 1.3.8 + object-assign: 4.1.1 + transitivePeerDependencies: + - supports-color + + koa-onerror@4.2.0: + dependencies: + escape-html: 1.0.3 + stream-wormhole: 1.1.0 + + koa-override@3.0.0: + dependencies: + methods: 1.1.2 + + koa-range@0.3.0: + dependencies: + stream-slice: 0.1.2 + + koa-session@6.4.0: + dependencies: + crc: 3.8.0 + debug: 4.4.3 + is-type-of: 1.4.0 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + koa-static-cache@5.1.4: + dependencies: + compressible: 2.0.18 + debug: 3.2.7 + fs-readdir-recursive: 1.1.0 + mime-types: 2.1.35 + mz: 2.7.0 + transitivePeerDependencies: + - supports-color + + koa-webpack-hot-middleware@1.0.3: + dependencies: + webpack-hot-middleware: 2.26.1 + + koa@1.7.1: + dependencies: + accepts: 1.3.8 + co: 4.6.0 + composition: 2.3.0 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.8.0 + debug: 2.6.9 + delegates: 1.0.0 + destroy: 1.2.0 + error-inject: 1.0.0 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + koa-compose: 2.5.1 + koa-is-json: 1.0.0 + mime-types: 2.1.35 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + koa@2.16.4: + dependencies: + accepts: 1.3.8 + cache-content-type: 1.0.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookies: 0.9.1 + debug: 4.4.3 + delegates: 1.0.0 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + fresh: 0.5.2 + http-assert: 1.5.0 + http-errors: 1.8.1 + is-generator-function: 1.1.2 + koa-compose: 4.1.0 + koa-convert: 2.0.0 + on-finished: 2.4.1 + only: 0.0.2 + parseurl: 1.3.3 + statuses: 1.5.0 + type-is: 1.6.18 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + last-call-webpack-plugin@3.0.0: + dependencies: + lodash: 4.18.1 + webpack-sources: 1.4.3 + + latest-version@3.1.0: + dependencies: + package-json: 4.0.1 + + latest-version@5.1.0: + dependencies: + package-json: 6.5.0 + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lcid@1.0.0: + dependencies: + invert-kv: 1.0.0 + + less-loader@4.1.0(less@3.9.0)(webpack@4.47.0): + dependencies: + clone: 2.1.2 + less: 3.9.0 + loader-utils: 1.4.2 + pify: 3.0.0 + webpack: 4.47.0 + + less@3.9.0: + dependencies: + clone: 2.1.2 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + mime: 1.6.0 + mkdirp: 0.5.6 + promise: 7.3.1 + request: 2.88.2 + source-map: 0.6.1 + + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lines-and-columns@1.2.4: {} + + livereload-js@3.4.1: {} + + livereload@0.9.3: + dependencies: + chokidar: 3.6.0 + livereload-js: 3.4.1 + opts: 2.0.2 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + load-json-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 2.2.0 + pify: 2.3.0 + pinkie-promise: 2.0.1 + strip-bom: 2.0.0 + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + loader-fs-cache@1.0.3: + dependencies: + find-cache-dir: 0.1.1 + mkdirp: 0.5.6 + + loader-runner@2.4.0: {} + + loader-utils@0.2.17: + dependencies: + big.js: 3.2.0 + emojis-list: 2.1.0 + json5: 0.5.1 + object-assign: 4.1.1 + + loader-utils@1.4.2: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.18.1: {} + + lodash._reinterpolate@3.0.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.clonedeepwith@4.5.0: {} + + lodash.debounce@4.0.8: {} + + lodash.defaults@4.2.0: {} + + lodash.get@4.4.2: {} + + lodash.has@4.5.2: {} + + lodash.ismatch@4.4.0: {} + + lodash.map@4.6.0: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.set@4.3.2: {} + + lodash.sortby@4.7.0: {} + + lodash.template@4.18.1: + dependencies: + lodash._reinterpolate: 3.0.0 + lodash.templatesettings: 4.2.0 + + lodash.templatesettings@4.2.0: + dependencies: + lodash._reinterpolate: 3.0.0 + + lodash.throttle@4.1.1: {} + + lodash.truncate@4.4.2: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + lodash@4.18.1: {} + + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + long-timeout@0.1.1: {} + + long@4.0.0: {} + + longest-streak@2.0.4: {} + + longest@2.0.1: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loud-rejection@1.6.0: + dependencies: + currently-unhandled: 0.4.1 + signal-exit: 3.0.7 + + lower-case-first@1.0.2: + dependencies: + lower-case: 1.1.4 + + lower-case@1.1.4: {} + + lowercase-keys@1.0.1: {} + + lowercase-keys@2.0.0: {} + + lowlight@1.20.0: + dependencies: + fault: 1.0.4 + highlight.js: 10.7.3 + + lru-cache@10.4.3: {} + + lru-cache@4.1.5: + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-queue@0.1.0: + dependencies: + es5-ext: 0.10.64 + + lru.min@1.1.4: {} + + luxon@3.7.2: {} + + make-dir@1.3.0: + dependencies: + pify: 3.0.0 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.0 + + make-error@1.3.6: {} + + map-cache@0.2.2: {} + + map-obj@1.0.1: {} + + map-obj@2.0.0: {} + + map-obj@4.3.0: {} + + map-stream@0.1.0: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + + marked@1.2.9: {} + + matcher@1.1.1: + dependencies: + escape-string-regexp: 1.0.5 + + material-colors@1.2.6: {} + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + md5.js@1.3.5: + dependencies: + hash-base: 3.0.5 + inherits: 2.0.4 + safe-buffer: 5.2.1 + + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + + mdast-util-definitions@4.0.0: + dependencies: + unist-util-visit: 2.0.3 + + mdast-util-find-and-replace@1.1.1: + dependencies: + escape-string-regexp: 4.0.0 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + mdast-util-from-markdown@0.8.5: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@0.1.3: + dependencies: + ccount: 1.1.0 + mdast-util-find-and-replace: 1.1.1 + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@0.2.3: + dependencies: + mdast-util-to-markdown: 0.6.5 + + mdast-util-gfm-table@0.1.6: + dependencies: + markdown-table: 2.0.0 + mdast-util-to-markdown: 0.6.5 + + mdast-util-gfm-task-list-item@0.1.6: + dependencies: + mdast-util-to-markdown: 0.6.5 + + mdast-util-gfm@0.1.2: + dependencies: + mdast-util-gfm-autolink-literal: 0.1.3 + mdast-util-gfm-strikethrough: 0.2.3 + mdast-util-gfm-table: 0.1.6 + mdast-util-gfm-task-list-item: 0.1.6 + mdast-util-to-markdown: 0.6.5 + transitivePeerDependencies: + - supports-color + + mdast-util-to-hast@10.2.0: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + mdast-util-definitions: 4.0.0 + mdurl: 1.0.1 + unist-builder: 2.0.3 + unist-util-generated: 1.1.6 + unist-util-position: 3.1.0 + unist-util-visit: 2.0.3 + + mdast-util-to-markdown@0.6.5: + dependencies: + '@types/unist': 2.0.11 + longest-streak: 2.0.4 + mdast-util-to-string: 2.0.0 + parse-entities: 2.0.0 + repeat-string: 1.6.1 + zwitch: 1.0.5 + + mdast-util-to-string@2.0.0: {} + + mdn-data@2.0.14: {} + + mdn-data@2.0.4: {} + + mdurl@1.0.1: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + medium-zoom@1.1.0: {} + + memoizee@0.4.17: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-weak-map: 2.0.3 + event-emitter: 0.3.5 + is-promise: 2.2.2 + lru-queue: 0.1.0 + next-tick: 1.1.0 + timers-ext: 0.1.8 + + memory-fs@0.4.1: + dependencies: + errno: 0.1.8 + readable-stream: 2.3.8 + + memory-fs@0.5.0: + dependencies: + errno: 0.1.8 + readable-stream: 2.3.8 + + meow@13.2.0: + optional: true + + meow@3.7.0: + dependencies: + camelcase-keys: 2.1.0 + decamelize: 1.2.0 + loud-rejection: 1.6.0 + map-obj: 1.0.1 + minimist: 1.2.8 + normalize-package-data: 2.5.0 + object-assign: 4.1.1 + read-pkg-up: 1.0.1 + redent: 1.0.0 + trim-newlines: 1.0.0 + + meow@5.0.0: + dependencies: + camelcase-keys: 4.2.0 + decamelize-keys: 1.1.1 + loud-rejection: 1.6.0 + minimist-options: 3.0.2 + normalize-package-data: 2.5.0 + read-pkg-up: 3.0.0 + redent: 2.0.0 + trim-newlines: 2.0.0 + yargs-parser: 10.1.0 + + meow@8.1.2: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + + meow@9.0.0: + dependencies: + '@types/minimist': 1.2.5 + camelcase-keys: 6.2.2 + decamelize: 1.2.0 + decamelize-keys: 1.1.1 + hard-rejection: 2.1.0 + minimist-options: 4.1.0 + normalize-package-data: 3.0.3 + read-pkg-up: 7.0.1 + redent: 3.0.0 + trim-newlines: 3.0.1 + type-fest: 0.18.1 + yargs-parser: 20.2.9 + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + merge-estraverse-visitors@1.0.0: + dependencies: + estraverse: 4.3.0 + + merge-source-map@1.1.0: + dependencies: + source-map: 0.6.1 + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + merge@2.1.1: {} + + method-override@2.3.10: + dependencies: + debug: 2.6.9 + methods: 1.1.2 + parseurl: 1.3.3 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromark-extension-gfm-autolink-literal@0.5.7: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm-strikethrough@0.6.5: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm-table@0.4.3: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm-tagfilter@0.3.0: {} + + micromark-extension-gfm-task-list-item@0.3.3: + dependencies: + micromark: 2.11.4 + transitivePeerDependencies: + - supports-color + + micromark-extension-gfm@0.3.3: + dependencies: + micromark: 2.11.4 + micromark-extension-gfm-autolink-literal: 0.5.7 + micromark-extension-gfm-strikethrough: 0.6.5 + micromark-extension-gfm-table: 0.4.3 + micromark-extension-gfm-tagfilter: 0.3.0 + micromark-extension-gfm-task-list-item: 0.3.3 + transitivePeerDependencies: + - supports-color + + micromark@2.11.4: + dependencies: + debug: 4.4.3 + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + + micromatch@3.1.10: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + miller-rabin@4.0.1: + dependencies: + bn.js: 4.12.3 + brorand: 1.1.0 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.3.4: {} + + mime@1.3.6: {} + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@1.2.0: {} + + mimic-fn@2.1.0: {} + + mimic-response@1.0.1: {} + + min-document@2.19.2: + dependencies: + dom-walk: 0.1.2 + + min-indent@1.0.1: {} + + mini-css-extract-plugin@0.9.0(webpack@4.47.0): + dependencies: + loader-utils: 1.4.2 + normalize-url: 1.9.1 + schema-utils: 1.0.0 + webpack: 4.47.0 + webpack-sources: 1.4.3 + + mini-store@2.0.0: + dependencies: + hoist-non-react-statics: 2.5.5 + prop-types: 15.8.1 + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + + mini-store@3.0.6(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + hoist-non-react-statics: 3.3.2 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + shallowequal: 1.1.0 + + minimalistic-assert@1.0.1: {} + + minimalistic-crypto-utils@1.0.1: {} + + minimatch@3.0.4: + dependencies: + brace-expansion: 1.1.14 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + minimist-options@3.0.2: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + + minimist-options@4.1.0: + dependencies: + arrify: 1.0.1 + is-plain-obj: 1.1.0 + kind-of: 6.0.3 + + minimist@1.2.7: {} + + minimist@1.2.8: {} + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.3: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mississippi@2.0.0: + dependencies: + concat-stream: 1.6.2 + duplexify: 3.7.1 + end-of-stream: 1.4.5 + flush-write-stream: 1.1.1 + from2: 2.3.0 + parallel-transform: 1.2.0 + pump: 2.0.1 + pumpify: 1.5.1 + stream-each: 1.2.3 + through2: 2.0.5 + + mississippi@3.0.0: + dependencies: + concat-stream: 1.6.2 + duplexify: 3.7.1 + end-of-stream: 1.4.5 + flush-write-stream: 1.1.1 + from2: 2.3.0 + parallel-transform: 1.2.0 + pump: 3.0.4 + pumpify: 1.5.1 + stream-each: 1.2.3 + through2: 2.0.5 + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp@0.5.4: + dependencies: + minimist: 1.2.8 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mocha@6.2.3: + dependencies: + ansi-colors: 3.2.3 + browser-stdout: 1.3.1 + debug: 3.2.6(supports-color@6.0.0) + diff: 3.5.0 + escape-string-regexp: 1.0.5 + find-up: 3.0.0 + glob: 7.1.3 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 3.13.1 + log-symbols: 2.2.0 + minimatch: 3.0.4 + mkdirp: 0.5.4 + ms: 2.1.1 + node-environment-flags: 1.0.5 + object.assign: 4.1.0 + strip-json-comments: 2.0.1 + supports-color: 6.0.0 + which: 1.3.1 + wide-align: 1.1.3 + yargs: 13.3.2 + yargs-parser: 13.1.2 + yargs-unparser: 1.6.0 + + mockjs@1.1.0: + dependencies: + commander: 14.0.3 + + modify-values@1.0.1: {} + + moment-timezone@0.5.48: + dependencies: + moment: 2.30.1 + + moment@2.30.1: {} + + morgan@1.6.1: + dependencies: + basic-auth: 1.0.4 + debug: 2.2.0 + depd: 1.0.1 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + + move-concurrently@1.0.1: + dependencies: + aproba: 1.2.0 + copy-concurrently: 1.0.5 + fs-write-stream-atomic: 1.0.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + run-queue: 1.0.3 + + ms@0.7.1: {} + + ms@0.7.2: {} + + ms@2.0.0: {} + + ms@2.1.1: {} + + ms@2.1.3: {} + + multi-stage-sourcemap@0.2.1: + dependencies: + source-map: 0.1.43 + + multimatch@2.1.0: + dependencies: + array-differ: 1.0.0 + array-union: 1.0.2 + arrify: 1.0.1 + minimatch: 3.1.5 + + multiparty@3.3.2: + dependencies: + readable-stream: 1.1.14 + stream-counter: 0.2.0 + + mustache@2.3.2: {} + + mutation-observer@1.0.3: {} + + mutationobserver-shim@0.3.7: {} + + mute-stream@0.0.7: {} + + mute-stream@0.0.8: {} + + mysql2@1.7.0: + dependencies: + denque: 1.5.1 + generate-function: 2.3.1 + iconv-lite: 0.5.2 + long: 4.0.0 + lru-cache: 5.1.1 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + mz-modules@1.0.0: + dependencies: + glob: 7.2.3 + ko-sleep: 1.1.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + + mz-modules@2.1.0: + dependencies: + glob: 7.2.3 + ko-sleep: 1.1.4 + mkdirp: 0.5.6 + pump: 3.0.4 + rimraf: 2.7.1 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + nan@2.27.0: + optional: true + + nanoid@3.3.12: {} + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + natural-compare@1.4.0: {} + + ndir@0.1.5: {} + + negotiator@0.5.3: {} + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + nested-error-stacks@2.1.1: {} + + next-tick@1.1.0: {} + + nice-try@1.0.5: {} + + no-case@2.3.2: + dependencies: + lower-case: 1.1.4 + + node-addon-api@7.1.1: + optional: true + + node-environment-flags@1.0.5: + dependencies: + object.getownpropertydescriptors: 2.1.9 + semver: 5.7.2 + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-fetch@1.7.3: + dependencies: + encoding: 0.1.13 + is-stream: 1.1.0 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-glob@1.2.0: + dependencies: + async: 1.5.2 + glob-to-regexp: 0.1.0 + + node-hex@1.0.1: {} + + node-homedir@1.1.1: {} + + node-http-server@8.1.6: {} + + node-libs-browser@2.2.1: + dependencies: + assert: 1.5.1 + browserify-zlib: 0.2.0 + buffer: 4.9.2 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + crypto-browserify: 3.12.1 + domain-browser: 1.2.0 + events: 3.3.0 + https-browserify: 1.0.0 + os-browserify: 0.3.0 + path-browserify: 0.0.1 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 2.3.8 + stream-browserify: 2.0.2 + stream-http: 2.8.3 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.0 + url: 0.11.4 + util: 0.11.1 + vm-browserify: 1.1.2 + + node-nightly-version@1.0.6: + dependencies: + isomorphic-fetch: 2.2.1 + meow: 3.7.0 + node-nightly-versions: 1.0.2 + + node-nightly-versions@1.0.2: + dependencies: + isomorphic-fetch: 2.2.1 + + node-noop@1.0.0: {} + + node-releases@2.0.45: {} + + node-schedule@2.1.1: + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 + + node-ssh@10.0.2: + dependencies: + make-dir: 3.1.0 + sb-promise-queue: 2.1.1 + sb-scandir: 3.1.1 + shell-escape: 0.2.0 + ssh2: 0.8.9 + + node-ssh@6.0.0: + dependencies: + p-map: 2.1.0 + sb-promisify: 2.0.2 + sb-scandir: 2.0.0 + shell-escape: 0.2.0 + ssh2: 0.8.9 + + node-tool-utils@1.6.0: + dependencies: + chalk: 2.4.2 + cross-port-killer: 1.4.0 + mkdirp: 0.5.6 + node-glob: 1.2.0 + node-http-server: 8.1.6 + opn: 5.5.0 + shelljs: 0.8.5 + + nodeinstall@0.1.6: + dependencies: + bytes: 2.5.0 + co: 4.6.0 + commander: 2.20.3 + debug: 2.6.9 + extend: 3.0.2 + mkdirp: 0.5.6 + node-nightly-version: 1.0.6 + only: 0.0.2 + progress: 2.0.3 + semver: 5.7.2 + tar: 2.2.2 + urllib: 2.44.0 + transitivePeerDependencies: + - proxy-agent + - supports-color + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.12 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.2 + semver: 7.8.0 + validate-npm-package-license: 3.0.4 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-url@1.9.1: + dependencies: + object-assign: 4.1.1 + prepend-http: 1.0.4 + query-string: 4.3.4 + sort-keys: 1.1.2 + + normalize-url@3.3.0: {} + + normalize-url@4.5.1: {} + + notepack.io@2.2.0: {} + + npm-install-webpack-plugin@4.0.5(webpack@4.47.0): + dependencies: + cross-spawn: 5.1.0 + json5: 0.5.1 + memory-fs: 0.4.1 + resolve: 1.22.12 + webpack: 4.47.0 + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nth-check@1.0.2: + dependencies: + boolbase: 1.0.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + num2fraction@1.2.2: {} + + number-is-nan@1.0.1: {} + + nwsapi@2.2.23: {} + + nyc@13.3.0: + dependencies: + istanbul-lib-instrument: 3.3.0 + transitivePeerDependencies: + - supports-color + + oauth-sign@0.9.0: {} + + object-assign@4.1.1: {} + + object-component@0.0.3: {} + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-hash@1.3.1: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.assign@4.1.0: + dependencies: + define-properties: 1.2.1 + function-bind: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + + object.getownpropertydescriptors@2.1.9: + dependencies: + array.prototype.reduce: 1.0.8 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + gopd: 1.2.0 + safe-array-concat: 1.1.4 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.hasown@1.1.4: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + omit.js@1.0.2: + dependencies: + babel-runtime: 6.26.0 + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + only@0.0.2: {} + + open@6.4.0: + dependencies: + is-wsl: 1.1.0 + + opencollective-postinstall@2.0.3: {} + + opener@1.5.2: {} + + opn@5.5.0: + dependencies: + is-wsl: 1.1.0 + + optimize-css-assets-webpack-plugin@5.0.8(webpack@4.47.0): + dependencies: + cssnano: 4.1.11 + last-call-webpack-plugin: 3.0.0 + webpack: 4.47.0 + + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + options@0.0.6: {} + + opts@2.0.2: {} + + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-browserify@0.3.0: {} + + os-homedir@1.0.2: {} + + os-locale@1.4.0: + dependencies: + lcid: 1.0.0 + + os-name@1.0.3: + dependencies: + osx-release: 1.1.0 + win-release: 1.1.1 + + os-tmpdir@1.0.2: {} + + osx-release@1.1.0: + dependencies: + minimist: 1.2.8 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-cancelable@1.1.0: {} + + p-event@4.2.0: + dependencies: + p-timeout: 3.2.0 + + p-finally@1.0.0: {} + + p-finally@2.0.1: {} + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-map@1.2.0: {} + + p-map@2.1.0: {} + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@4.1.0: {} + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + package-json@4.0.1: + dependencies: + got: 6.7.1 + registry-auth-token: 3.4.0 + registry-url: 3.1.0 + semver: 5.7.2 + + package-json@6.5.0: + dependencies: + got: 9.6.0 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 6.3.1 + + pako@1.0.11: {} + + parallel-transform@1.2.0: + dependencies: + cyclist: 1.0.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + + param-case@2.1.1: + dependencies: + no-case: 2.3.2 + + parameter@2.4.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parent-require@1.0.0: {} + + parse-asn1@5.1.9: + dependencies: + asn1.js: 4.10.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.5 + safe-buffer: 5.2.1 + + parse-entities@2.0.0: + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + + parse-json@2.2.0: + dependencies: + error-ex: 1.3.4 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-passwd@1.0.0: {} + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@5.1.0: {} + + parse5@6.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parsejson@0.0.3: + dependencies: + better-assert: 1.0.2 + + parseqs@0.0.5: + dependencies: + better-assert: 1.0.2 + + parseqs@0.0.6: {} + + parseuri@0.0.5: + dependencies: + better-assert: 1.0.2 + + parseuri@0.0.6: {} + + parseurl@1.3.3: {} + + pascal-case@2.0.1: + dependencies: + camel-case: 3.0.0 + upper-case-first: 1.1.2 + + pascalcase@0.1.1: {} + + path-browserify@0.0.1: {} + + path-case@2.1.1: + dependencies: + no-case: 2.3.2 + + path-dirname@1.0.2: {} + + path-exists@2.1.0: + dependencies: + pinkie-promise: 2.0.1 + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-is-inside@1.0.2: {} + + path-key@2.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-to-regexp@0.1.13: {} + + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + + path-to-regexp@8.4.2: {} + + path-type@1.1.0: + dependencies: + graceful-fs: 4.2.11 + pify: 2.3.0 + pinkie-promise: 2.0.1 + + path-type@3.0.0: + dependencies: + pify: 3.0.0 + + path-type@4.0.0: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + pause@0.1.0: {} + + pbkdf2@3.1.5: + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.3 + safe-buffer: 5.2.1 + sha.js: 2.4.12 + to-buffer: 1.2.2 + + pend@1.2.0: {} + + performance-now@2.1.0: {} + + picocolors@0.2.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: + optional: true + + pify@2.3.0: {} + + pify@3.0.0: {} + + pify@4.0.1: {} + + pinkie-promise@2.0.1: + dependencies: + pinkie: 2.0.4 + + pinkie@2.0.4: {} + + pkce-challenge@5.0.1: {} + + pkg-dir@1.0.0: + dependencies: + find-up: 1.1.2 + + pkg-dir@2.0.0: + dependencies: + find-up: 2.1.0 + + pkg-dir@3.0.0: + dependencies: + find-up: 3.0.0 + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + platform@1.3.6: {} + + please-upgrade-node@3.2.0: + dependencies: + semver-compare: 1.0.0 + + pluralize@7.0.0: {} + + pn@1.1.0: {} + + posix-character-classes@0.1.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss-calc@7.0.5: + dependencies: + postcss: 7.0.39 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-colormin@4.0.3: + dependencies: + browserslist: 4.28.2 + color: 3.2.1 + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-convert-values@4.0.1: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-discard-comments@4.0.2: + dependencies: + postcss: 7.0.39 + + postcss-discard-duplicates@4.0.2: + dependencies: + postcss: 7.0.39 + + postcss-discard-empty@4.0.1: + dependencies: + postcss: 7.0.39 + + postcss-discard-overridden@4.0.1: + dependencies: + postcss: 7.0.39 + + postcss-less@6.0.0(postcss@8.4.14): + dependencies: + postcss: 8.4.14 + + postcss-load-config@2.1.2: + dependencies: + cosmiconfig: 5.2.1 + import-cwd: 2.1.0 + + postcss-loader@3.0.0: + dependencies: + loader-utils: 1.4.2 + postcss: 7.0.39 + postcss-load-config: 2.1.2 + schema-utils: 1.0.0 + + postcss-media-query-parser@0.2.3: {} + + postcss-merge-longhand@4.0.11: + dependencies: + css-color-names: 0.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + stylehacks: 4.0.3 + + postcss-merge-rules@4.0.3: + dependencies: + browserslist: 4.28.2 + caniuse-api: 3.0.0 + cssnano-util-same-parent: 4.0.1 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + vendors: 1.0.4 + + postcss-minify-font-values@4.0.2: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-minify-gradients@4.0.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + is-color-stop: 1.1.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-minify-params@4.0.2: + dependencies: + alphanum-sort: 1.0.2 + browserslist: 4.28.2 + cssnano-util-get-arguments: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + uniqs: 2.0.0 + + postcss-minify-selectors@4.0.2: + dependencies: + alphanum-sort: 1.0.2 + has: 1.0.4 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + + postcss-modules-extract-imports@2.0.0: + dependencies: + postcss: 7.0.39 + + postcss-modules-local-by-default@3.0.3: + dependencies: + icss-utils: 4.1.1 + postcss: 7.0.39 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@2.2.0: + dependencies: + postcss: 7.0.39 + postcss-selector-parser: 6.1.2 + + postcss-modules-values@3.0.0: + dependencies: + icss-utils: 4.1.1 + postcss: 7.0.39 + + postcss-normalize-charset@4.0.1: + dependencies: + postcss: 7.0.39 + + postcss-normalize-display-values@4.0.2: + dependencies: + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-positions@4.0.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-repeat-style@4.0.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-string@4.0.2: + dependencies: + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-timing-functions@4.0.2: + dependencies: + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-unicode@4.0.1: + dependencies: + browserslist: 4.28.2 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-url@4.0.1: + dependencies: + is-absolute-url: 2.1.0 + normalize-url: 3.3.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-normalize-whitespace@4.0.2: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-ordered-values@4.1.2: + dependencies: + cssnano-util-get-arguments: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-reduce-initial@4.0.3: + dependencies: + browserslist: 4.28.2 + caniuse-api: 3.0.0 + has: 1.0.4 + postcss: 7.0.39 + + postcss-reduce-transforms@4.0.2: + dependencies: + cssnano-util-get-match: 4.0.0 + has: 1.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + + postcss-scss@4.0.4(postcss@8.4.14): + dependencies: + postcss: 8.4.14 + + postcss-selector-parser@3.1.2: + dependencies: + dot-prop: 5.3.0 + indexes-of: 1.0.1 + uniq: 1.0.1 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@7.0.1(postcss@8.4.14): + dependencies: + postcss: 8.4.14 + + postcss-svgo@4.0.3: + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + svgo: 1.3.2 + + postcss-unique-selectors@4.0.1: + dependencies: + alphanum-sort: 1.0.2 + postcss: 7.0.39 + uniqs: 2.0.0 + + postcss-value-parser@3.3.1: {} + + postcss-value-parser@4.2.0: {} + + postcss@7.0.39: + dependencies: + picocolors: 0.2.1 + source-map: 0.6.1 + + postcss@8.4.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + power-assert-context-formatter@1.2.0: + dependencies: + core-js: 2.6.12 + power-assert-context-traversal: 1.2.0 + + power-assert-context-reducer-ast@1.2.0: + dependencies: + acorn: 5.7.4 + acorn-es7-plugin: 1.1.7 + core-js: 2.6.12 + espurify: 1.8.1 + estraverse: 4.3.0 + + power-assert-context-traversal@1.2.0: + dependencies: + core-js: 2.6.12 + estraverse: 4.3.0 + + power-assert-formatter@1.4.1: + dependencies: + core-js: 2.6.12 + power-assert-context-formatter: 1.2.0 + power-assert-context-reducer-ast: 1.2.0 + power-assert-renderer-assertion: 1.2.0 + power-assert-renderer-comparison: 1.2.0 + power-assert-renderer-diagram: 1.2.0 + power-assert-renderer-file: 1.2.0 + + power-assert-renderer-assertion@1.2.0: + dependencies: + power-assert-renderer-base: 1.1.1 + power-assert-util-string-width: 1.2.0 + + power-assert-renderer-base@1.1.1: {} + + power-assert-renderer-comparison@1.2.0: + dependencies: + core-js: 2.6.12 + diff-match-patch: 1.0.5 + power-assert-renderer-base: 1.1.1 + stringifier: 1.4.1 + type-name: 2.0.2 + + power-assert-renderer-diagram@1.2.0: + dependencies: + core-js: 2.6.12 + power-assert-renderer-base: 1.1.1 + power-assert-util-string-width: 1.2.0 + stringifier: 1.4.1 + + power-assert-renderer-file@1.2.0: + dependencies: + power-assert-renderer-base: 1.1.1 + + power-assert-util-string-width@1.2.0: + dependencies: + eastasianwidth: 0.2.0 + + power-assert@1.6.1: + dependencies: + define-properties: 1.2.1 + empower: 1.3.1 + power-assert-formatter: 1.4.1 + universal-deep-strict-equal: 1.2.2 + xtend: 4.0.2 + + prelude-ls@1.1.2: {} + + prelude-ls@1.2.1: {} + + prepend-http@1.0.4: {} + + prepend-http@2.0.0: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@2.7.1: {} + + pretty-bytes@4.0.2: {} + + pretty-error@2.1.2: + dependencies: + lodash: 4.18.1 + renderkid: 2.0.7 + + prismjs@1.27.0: {} + + prismjs@1.30.0: {} + + private@0.1.8: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + progress-bar-webpack-plugin@1.12.1(webpack@4.47.0): + dependencies: + chalk: 1.1.3 + object.assign: 4.1.7 + progress: 1.1.8 + webpack: 4.47.0 + + progress@1.1.8: {} + + progress@2.0.3: {} + + promise-inflight@1.0.1(bluebird@3.7.2): + optionalDependencies: + bluebird: 3.7.2 + + promise@7.3.1: + dependencies: + asap: 2.0.6 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@5.6.0: + dependencies: + xtend: 4.0.2 + + proto-list@1.2.4: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + prr@1.0.1: {} + + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + + pseudomap@1.0.2: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + public-encrypt@4.0.3: + dependencies: + bn.js: 4.12.3 + browserify-rsa: 4.1.1 + create-hash: 1.2.0 + parse-asn1: 5.1.9 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + pump@2.0.1: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pumpify@1.5.1: + dependencies: + duplexify: 3.7.1 + inherits: 2.0.4 + pump: 2.0.1 + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + pupa@2.1.1: + dependencies: + escape-goat: 2.1.1 + + q@1.5.1: {} + + qs@4.0.0: {} + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + qs@6.5.5: {} + + query-string@4.3.4: + dependencies: + object-assign: 4.1.1 + strict-uri-encode: 1.1.0 + + querystring-es3@0.2.1: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + quick-lru@1.1.0: {} + + quick-lru@4.0.1: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + + random-bytes@1.0.0: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + randomfill@1.0.4: + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + + range-parser@1.0.3: {} + + range-parser@1.2.1: {} + + raw-body@2.1.7: + dependencies: + bytes: 2.4.0 + iconv-lite: 0.4.13 + unpipe: 1.0.0 + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc-align@2.4.5: + dependencies: + babel-runtime: 6.26.0 + dom-align: 1.12.4 + prop-types: 15.8.1 + rc-util: 4.21.1 + + rc-align@4.0.15(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + dom-align: 1.12.4 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + resize-observer-polyfill: 1.5.1 + + rc-animate@2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + css-animation: 1.6.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-lifecycles-compat: 3.0.4 + + rc-animate@3.1.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@ant-design/css-animation': 1.7.3 + classnames: 2.2.6 + raf: 3.4.1 + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-calendar@9.15.11(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + moment: 2.30.1 + prop-types: 15.8.1 + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - react + - react-dom + + rc-cascader@0.17.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + array-tree-filter: 2.1.0 + prop-types: 15.8.1 + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + shallow-equal: 1.2.1 + warning: 4.0.3 + transitivePeerDependencies: + - react + - react-dom + + rc-cascader@1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + array-tree-filter: 2.1.0 + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + warning: 4.0.3 + + rc-checkbox@2.1.8: + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + react-lifecycles-compat: 3.0.4 + + rc-checkbox@2.3.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-collapse@1.11.8(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + css-animation: 1.6.1 + prop-types: 15.8.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react-is: 16.13.1 + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + transitivePeerDependencies: + - react + - react-dom + + rc-collapse@3.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + shallowequal: 1.1.0 + + rc-dialog@7.6.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + transitivePeerDependencies: + - react + - react-dom + + rc-dialog@8.5.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-dialog@8.6.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-drawer@3.1.3(react@16.9.0): + dependencies: + classnames: 2.2.6 + rc-util: 4.21.1 + react: 16.9.0 + react-lifecycles-compat: 3.0.4 + + rc-drawer@4.3.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-dropdown@2.4.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - react + - react-dom + + rc-dropdown@3.2.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-editor-core@0.8.10(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + draft-js: 0.10.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + immutable: 3.7.6 + lodash: 4.18.1 + prop-types: 15.8.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + setimmediate: 1.0.5 + + rc-editor-mention@1.1.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + dom-scroll-into-view: 1.2.1 + draft-js: 0.10.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + immutable: 3.7.6 + prop-types: 15.8.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-editor-core: 0.8.10(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-field-form@1.20.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + async-validator: 3.5.2 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-form@2.4.12(prop-types@15.8.1): + dependencies: + async-validator: 1.11.5 + babel-runtime: 6.26.0 + create-react-class: 15.7.0 + dom-scroll-into-view: 1.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.18.1 + prop-types: 15.8.1 + rc-util: 4.21.1 + react-is: 16.13.1 + warning: 4.0.3 + + rc-hammerjs@0.6.10: + dependencies: + babel-runtime: 6.26.0 + hammerjs: 2.0.8 + prop-types: 15.8.1 + + rc-image@5.2.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-dialog: 8.6.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-input-number@4.5.9: + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + rc-util: 4.21.1 + rmc-feedback: 2.0.0 + + rc-input-number@7.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-mentions@0.4.2(prop-types@15.8.1)(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@ant-design/create-react-context': 0.2.6(prop-types@15.8.1)(react@16.9.0) + classnames: 2.2.6 + rc-menu: 7.5.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react: 16.9.0 + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - prop-types + - react-dom + + rc-mentions@1.5.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-menu: 8.10.8(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-textarea: 0.3.7(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-menu@7.5.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + dom-scroll-into-view: 1.2.1 + mini-store: 2.0.0 + mutationobserver-shim: 0.3.7 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + resize-observer-polyfill: 1.5.1 + shallowequal: 1.1.0 + + rc-menu@8.10.8(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + mini-store: 3.0.6(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + resize-observer-polyfill: 1.5.1 + shallowequal: 1.1.0 + + rc-motion@2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-notification@3.3.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + transitivePeerDependencies: + - react + - react-dom + + rc-notification@4.5.7(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-overflow@1.5.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-pagination@1.20.15: + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + react-lifecycles-compat: 3.0.4 + + rc-pagination@3.1.17(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-picker@2.5.19(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + date-fns: 2.30.0 + dayjs: 1.11.20 + moment: 2.30.1 + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + shallowequal: 1.1.0 + + rc-progress@2.5.3: + dependencies: + babel-runtime: 6.26.0 + prop-types: 15.8.1 + + rc-progress@3.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-rate@2.5.1: + dependencies: + classnames: 2.2.6 + prop-types: 15.8.1 + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + + rc-rate@2.9.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-resize-observer@0.1.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + resize-observer-polyfill: 1.5.1 + + rc-resize-observer@1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + resize-observer-polyfill: 1.5.1 + + rc-select@12.1.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-overflow: 1.5.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-virtual-list: 3.19.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-select@9.2.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + component-classes: 1.2.6 + dom-scroll-into-view: 1.2.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-menu: 7.5.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + warning: 4.0.3 + transitivePeerDependencies: + - react + - react-dom + + rc-slider@8.7.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + rc-tooltip: 3.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + warning: 4.0.3 + transitivePeerDependencies: + - react + - react-dom + + rc-slider@9.7.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-tooltip: 5.1.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + shallowequal: 1.1.0 + + rc-steps@3.5.0: + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + lodash: 4.18.1 + prop-types: 15.8.1 + + rc-steps@4.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-switch@1.9.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + prop-types: 15.8.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-lifecycles-compat: 3.0.4 + + rc-switch@3.2.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-table@6.10.15(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + component-classes: 1.2.6 + lodash: 4.18.1 + mini-store: 2.0.0 + prop-types: 15.8.1 + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + + rc-table@7.13.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + shallowequal: 1.1.0 + + rc-tabs@11.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-dropdown: 3.2.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-menu: 8.10.8(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-resize-observer: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-tabs@9.7.0(react@16.9.0): + dependencies: + '@ant-design/create-react-context': 0.2.6(prop-types@15.8.1)(react@16.9.0) + babel-runtime: 6.26.0 + classnames: 2.2.6 + lodash: 4.18.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-hammerjs: 0.6.10 + rc-util: 4.21.1 + react: 16.9.0 + react-lifecycles-compat: 3.0.4 + resize-observer-polyfill: 1.5.1 + warning: 4.0.3 + + rc-textarea@0.3.7(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + shallowequal: 1.1.0 + + rc-time-picker@3.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + moment: 2.30.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - react + - react-dom + + rc-tooltip@3.7.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + prop-types: 15.8.1 + rc-trigger: 2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + transitivePeerDependencies: + - react + - react-dom + + rc-tooltip@5.1.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + rc-trigger: 5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-tree-select@2.9.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + dom-scroll-into-view: 1.2.1 + prop-types: 15.8.1 + raf: 3.4.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tree: 2.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-trigger: 3.0.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + warning: 4.0.3 + transitivePeerDependencies: + - react + - react-dom + + rc-tree-select@4.3.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-select: 12.1.13(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-tree: 4.1.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-tree@2.1.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@ant-design/create-react-context': 0.2.6(prop-types@15.8.1)(react@16.9.0) + classnames: 2.2.6 + prop-types: 15.8.1 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-lifecycles-compat: 3.0.4 + warning: 4.0.3 + + rc-tree@4.1.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-virtual-list: 3.19.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-trigger@2.6.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + rc-align: 2.4.5 + rc-animate: 2.11.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + react-lifecycles-compat: 3.0.4 + transitivePeerDependencies: + - react + - react-dom + + rc-trigger@3.0.0(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + raf: 3.4.1 + rc-align: 2.4.5 + rc-animate: 3.1.1(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 4.21.1 + transitivePeerDependencies: + - react + - react-dom + + rc-trigger@5.3.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-align: 4.0.15(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-motion: 2.9.5(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-upload@2.9.4: + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + prop-types: 15.8.1 + warning: 4.0.3 + + rc-upload@4.3.6(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc-util@4.21.1: + dependencies: + add-dom-event-listener: 1.1.0 + prop-types: 15.8.1 + react-is: 16.13.1 + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + + rc-util@5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-is: 18.3.1 + + rc-virtual-list@3.19.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + rc-util: 5.44.4(react-dom@16.14.0(react@16.9.0))(react@16.9.0) + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-codemirror2@7.3.0(codemirror@5.65.21)(react@16.9.0): + dependencies: + codemirror: 5.65.21 + react: 16.9.0 + + react-color@2.19.3(react@16.9.0): + dependencies: + '@icons/material': 0.2.4(react@16.9.0) + lodash: 4.18.1 + lodash-es: 4.18.1 + material-colors: 1.2.6 + prop-types: 15.8.1 + react: 16.9.0 + reactcss: 1.2.3(react@16.9.0) + tinycolor2: 1.6.0 + + react-dom@16.14.0(react@16.9.0): + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + react: 16.9.0 + scheduler: 0.19.1 + + react-entry-template-loader@1.0.3: + dependencies: + loader-utils: 1.4.2 + + react-hot-loader@4.13.1(@types/react@16.14.70)(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + fast-levenshtein: 2.0.6 + global: 4.4.0 + hoist-non-react-statics: 3.3.2 + loader-utils: 2.0.4 + prop-types: 15.8.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + react-lifecycles-compat: 3.0.4 + shallowequal: 1.1.0 + source-map: 0.7.6 + optionalDependencies: + '@types/react': 16.14.70 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-lazy-load@3.1.14(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + eventlistener: 0.0.1 + lodash.debounce: 4.0.8 + lodash.throttle: 4.1.1 + prop-types: 15.8.1 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + + react-lifecycles-compat@3.0.4: {} + + react-loadable@5.5.0(react@16.9.0): + dependencies: + prop-types: 15.8.1 + react: 16.9.0 + + react-markdown@6.0.3(@types/react@16.14.70)(react@16.9.0): + dependencies: + '@types/hast': 2.3.10 + '@types/react': 16.14.70 + '@types/unist': 2.0.11 + comma-separated-tokens: 1.0.8 + prop-types: 15.8.1 + property-information: 5.6.0 + react: 16.9.0 + react-is: 17.0.2 + remark-parse: 9.0.0 + remark-rehype: 8.1.0 + space-separated-tokens: 1.1.5 + style-to-object: 0.3.0 + unified: 9.2.2 + unist-util-visit: 2.0.3 + vfile: 4.2.1 + transitivePeerDependencies: + - supports-color + + react-redux@7.2.9(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + '@types/react-redux': 7.1.34 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 16.9.0 + react-is: 17.0.2 + optionalDependencies: + react-dom: 16.14.0(react@16.9.0) + + react-router-config@1.0.0-beta.4(react-router@4.3.1(react@16.9.0))(react@16.9.0): + dependencies: + react: 16.9.0 + react-router: 4.3.1(react@16.9.0) + + react-router-dom@4.3.1(react@16.9.0): + dependencies: + history: 4.10.1 + invariant: 2.2.4 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 16.9.0 + react-router: 4.3.1(react@16.9.0) + warning: 4.0.3 + + react-router-redux@4.0.8: {} + + react-router@4.3.1(react@16.9.0): + dependencies: + history: 4.10.1 + hoist-non-react-statics: 2.5.5 + invariant: 2.2.4 + loose-envify: 1.4.0 + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 16.9.0 + warning: 4.0.3 + + react-slick@0.25.2(react-dom@16.14.0(react@16.9.0))(react@16.9.0): + dependencies: + classnames: 2.2.6 + enquire.js: 2.1.6 + json2mq: 0.2.0 + lodash.debounce: 4.0.8 + react: 16.9.0 + react-dom: 16.14.0(react@16.9.0) + resize-observer-polyfill: 1.5.1 + + react-syntax-highlighter@15.6.6(react@16.9.0): + dependencies: + '@babel/runtime': 7.29.2 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 16.9.0 + refractor: 3.6.0 + + react@16.9.0: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + + reactcss@1.2.3(react@16.9.0): + dependencies: + lodash: 4.18.1 + react: 16.9.0 + + read-pkg-up@1.0.1: + dependencies: + find-up: 1.1.2 + read-pkg: 1.1.0 + + read-pkg-up@3.0.0: + dependencies: + find-up: 2.1.0 + read-pkg: 3.0.0 + + read-pkg-up@4.0.0: + dependencies: + find-up: 3.0.0 + read-pkg: 3.0.0 + + read-pkg-up@5.0.0: + dependencies: + find-up: 3.0.0 + read-pkg: 5.2.0 + + read-pkg-up@7.0.1: + dependencies: + find-up: 4.1.0 + read-pkg: 5.2.0 + type-fest: 0.8.1 + + read-pkg@1.1.0: + dependencies: + load-json-file: 1.1.0 + normalize-package-data: 2.5.0 + path-type: 1.1.0 + + read-pkg@3.0.0: + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + + read-pkg@5.2.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 2.5.0 + parse-json: 5.2.0 + type-fest: 0.6.0 + + readable-stream@1.1.14: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@2.2.1: + dependencies: + graceful-fs: 4.2.11 + micromatch: 3.1.10 + readable-stream: 2.3.8 + transitivePeerDependencies: + - supports-color + optional: true + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@4.1.2: {} + + ready-callback@2.1.0: + dependencies: + debug: 2.6.9 + get-ready: 2.0.1 + once: 1.4.0 + uuid: 3.4.0 + transitivePeerDependencies: + - supports-color + + rechoir@0.6.2: + dependencies: + resolve: 1.22.12 + + redent@1.0.0: + dependencies: + indent-string: 2.1.0 + strip-indent: 1.0.1 + + redent@2.0.0: + dependencies: + indent-string: 3.2.0 + strip-indent: 2.0.0 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redis-commands@1.7.0: {} + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + redis@3.1.2: + dependencies: + denque: 1.5.1 + redis-commands: 1.7.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + + redux-devtools-extension@2.13.9(redux@4.2.1): + dependencies: + redux: 4.2.1 + + redux-thunk@2.4.2(redux@4.2.1): + dependencies: + redux: 4.2.1 + + redux@4.2.1: + dependencies: + '@babel/runtime': 7.29.2 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + refractor@3.6.0: + dependencies: + hastscript: 6.0.0 + parse-entities: 2.0.0 + prismjs: 1.27.0 + + regenerate@1.4.2: {} + + regenerator-runtime@0.10.5: {} + + regenerator-runtime@0.11.1: {} + + regenerator-runtime@0.13.11: {} + + regenerator-transform@0.10.1: + dependencies: + babel-runtime: 6.26.0 + babel-types: 6.26.0 + private: 0.1.8 + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpp@1.1.0: {} + + regexpp@3.2.0: {} + + regexpu-core@2.0.0: + dependencies: + regenerate: 1.4.2 + regjsgen: 0.2.0 + regjsparser: 0.1.5 + + registry-auth-token@3.4.0: + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + + registry-auth-token@4.2.2: + dependencies: + rc: 1.2.8 + + registry-url@3.1.0: + dependencies: + rc: 1.2.8 + + registry-url@5.1.0: + dependencies: + rc: 1.2.8 + + regjsgen@0.2.0: {} + + regjsparser@0.1.5: + dependencies: + jsesc: 0.5.0 + + relateurl@0.2.7: {} + + remark-gfm@1.0.0: + dependencies: + mdast-util-gfm: 0.1.2 + micromark-extension-gfm: 0.3.3 + transitivePeerDependencies: + - supports-color + + remark-parse@9.0.0: + dependencies: + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@8.1.0: + dependencies: + mdast-util-to-hast: 10.2.0 + + remove-trailing-separator@1.1.0: {} + + renderkid@2.0.7: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.18.1 + strip-ansi: 3.0.1 + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + repeating@2.0.1: + dependencies: + is-finite: 1.1.0 + + request-promise-core@1.1.4(request@2.88.2): + dependencies: + lodash: 4.18.1 + request: 2.88.2 + + request-promise-native@1.0.9(request@2.88.2): + dependencies: + request: 2.88.2 + request-promise-core: 1.1.4(request@2.88.2) + stealthy-require: 1.1.1 + tough-cookie: 2.5.0 + + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.5 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@1.0.1: {} + + require-main-filename@2.0.0: {} + + require-uncached@1.0.3: + dependencies: + caller-path: 0.1.0 + resolve-from: 1.0.1 + + requires-port@1.0.0: {} + + resize-observer-polyfill@1.5.1: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-files@1.0.2: + dependencies: + crequire: 1.8.1 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + resolve-from@1.0.1: {} + + resolve-from@3.0.0: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-global@1.0.0: + dependencies: + global-dirs: 0.1.1 + + resolve-pathname@3.0.0: {} + + resolve-pkg@2.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-url@0.2.1: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + response-time@2.3.4: + dependencies: + depd: 2.0.0 + on-headers: 1.1.0 + + responselike@1.0.2: + dependencies: + lowercase-keys: 1.0.1 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + ret@0.1.15: {} + + retry-as-promised@2.3.2: + dependencies: + bluebird: 3.7.2 + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + + reusify@1.1.0: {} + + rgb-regex@1.0.1: {} + + rgba-regex@1.0.0: {} + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + ripemd160@2.0.3: + dependencies: + hash-base: 3.1.2 + inherits: 2.0.4 + + rmc-feedback@2.0.0: + dependencies: + babel-runtime: 6.26.0 + classnames: 2.2.6 + + rndm@1.2.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-async@2.4.1: {} + + run-node@1.0.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + run-queue@1.0.3: + dependencies: + aproba: 1.2.0 + + runscript@1.6.0: + dependencies: + is-type-of: 1.4.0 + + rx-lite-aggregates@4.0.8: + dependencies: + rx-lite: 4.0.8 + + rx-lite@4.0.8: {} + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + safe-timers@1.1.0: {} + + safer-buffer@2.1.2: {} + + sass-loader@10.2.0(sass@1.99.0)(webpack@4.47.0): + dependencies: + klona: 2.0.6 + loader-utils: 2.0.4 + neo-async: 2.6.2 + schema-utils: 3.3.0 + semver: 7.8.0 + webpack: 4.47.0 + optionalDependencies: + sass: 1.99.0 + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + sax@1.2.4: {} + + saxes@3.1.11: + dependencies: + xmlchars: 2.2.0 + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + sb-promise-queue@2.1.1: {} + + sb-promisify@2.0.2: {} + + sb-scandir@2.0.0: + dependencies: + p-map: 1.2.0 + sb-promisify: 2.0.2 + + sb-scandir@3.1.1: + dependencies: + sb-promise-queue: 2.1.1 + + scheduler@0.19.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + scheduler@0.20.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + + schema-utils@0.3.0: + dependencies: + ajv: 5.5.2 + + schema-utils@0.4.7: + dependencies: + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@1.0.0: + dependencies: + ajv: 6.15.0 + ajv-errors: 1.0.1(ajv@6.15.0) + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@2.7.1: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + scmp@2.1.0: {} + + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + + sdk-base@2.0.1: + dependencies: + get-ready: 1.0.0 + + sdk-base@3.6.0: + dependencies: + await-event: 2.1.0 + await-first: 1.0.0 + co: 4.6.0 + is-type-of: 1.4.0 + + sdk-base@4.2.1: + dependencies: + await-event: 2.1.0 + await-first: 1.0.0 + co: 4.6.0 + p-timeout: 4.1.0 + + semver-compare@1.0.0: {} + + semver-diff@2.1.0: + dependencies: + semver: 5.7.2 + + semver-diff@3.1.1: + dependencies: + semver: 6.3.1 + + semver@5.7.2: {} + + semver@6.3.0: {} + + semver@6.3.1: {} + + semver@7.8.0: {} + + send@0.13.2: + dependencies: + debug: 2.2.0 + depd: 1.1.2 + destroy: 1.0.4 + escape-html: 1.0.3 + etag: 1.7.0 + fresh: 0.3.0 + http-errors: 1.3.1 + mime: 1.3.4 + ms: 0.7.1 + on-finished: 2.3.0 + range-parser: 1.0.3 + statuses: 1.2.1 + transitivePeerDependencies: + - supports-color + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + sendmessage@1.1.0: {} + + sentence-case@2.1.1: + dependencies: + no-case: 2.3.2 + upper-case-first: 1.1.2 + + seq-queue@0.0.5: {} + + sequelize-cli@5.5.1: + dependencies: + bluebird: 3.7.2 + cli-color: 1.4.0 + fs-extra: 7.0.1 + js-beautify: 1.15.4 + lodash: 4.18.1 + resolve: 1.22.12 + umzug: 2.3.0 + yargs: 13.3.2 + + sequelize@4.44.4: + dependencies: + bluebird: 3.7.2 + cls-bluebird: 2.1.0 + debug: 3.2.7 + depd: 1.1.2 + dottie: 2.0.7 + generic-pool: 3.5.0 + inflection: 1.12.0 + lodash: 4.18.1 + moment: 2.30.1 + moment-timezone: 0.5.48 + retry-as-promised: 2.3.2 + semver: 5.7.2 + terraformer-wkt-parser: 1.2.1 + toposort-class: 1.0.1 + uuid: 3.4.0 + validator: 10.11.0 + wkx: 0.4.8 + transitivePeerDependencies: + - supports-color + + serialize-javascript@1.9.1: {} + + serialize-javascript@2.1.2: {} + + serialize-javascript@4.0.0: + dependencies: + randombytes: 2.1.0 + + serialize-json@1.0.3: + dependencies: + debug: 3.2.7 + is-type-of: 1.4.0 + utility: 1.18.0 + transitivePeerDependencies: + - supports-color + + serve-favicon@2.3.2: + dependencies: + etag: 1.7.0 + fresh: 0.3.0 + ms: 0.7.2 + parseurl: 1.3.3 + + serve-index@1.7.3: + dependencies: + accepts: 1.2.13 + batch: 0.5.3 + debug: 2.2.0 + escape-html: 1.0.3 + http-errors: 1.3.1 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.10.3: + dependencies: + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.13.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + server-side-render-resource@1.2.0: + dependencies: + serialize-javascript: 4.0.0 + + service-worker-precache-webpack-plugin@1.3.5: + dependencies: + md5: 2.3.0 + sw-precache: 5.2.1 + uglify-es: 3.3.9 + webpack-merge: 4.2.2 + + serviceworker-cache-polyfill@4.0.0: {} + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.2 + + shallow-equal@1.2.1: {} + + shallowequal@1.1.0: {} + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@1.0.0: {} + + shebang-regex@3.0.0: {} + + shell-escape@0.2.0: {} + + shelljs@0.7.8: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + shimmer@1.2.1: {} + + should-send-same-site-none@2.0.5: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + slash@1.0.0: {} + + slash@2.0.0: {} + + slash@3.0.0: {} + + slice-ansi@1.0.0: + dependencies: + is-fullwidth-code-point: 2.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + snake-case@2.1.0: + dependencies: + no-case: 2.3.2 + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + socket.io-adapter@1.1.2: {} + + socket.io-adapter@2.5.7: + dependencies: + debug: 4.4.3 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@1.7.0: + dependencies: + backo2: 1.0.2 + component-bind: 1.0.0 + component-emitter: 1.2.1 + debug: 2.3.3 + engine.io-client: 1.8.1 + has-binary: 0.1.7 + indexof: 0.0.1 + object-component: 0.0.3 + parseuri: 0.0.5 + socket.io-parser: 2.3.1 + to-array: 0.1.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@2.5.0: + dependencies: + backo2: 1.0.2 + component-bind: 1.0.0 + component-emitter: 1.3.1 + debug: 3.1.0 + engine.io-client: 3.5.6 + has-binary2: 1.0.3 + indexof: 0.0.1 + parseqs: 0.0.6 + parseuri: 0.0.6 + socket.io-parser: 3.3.5 + to-array: 0.1.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@2.3.1: + dependencies: + component-emitter: 1.1.2 + debug: 2.2.0 + isarray: 0.0.1 + json3: 3.3.2 + transitivePeerDependencies: + - supports-color + + socket.io-parser@3.3.5: + dependencies: + component-emitter: 1.3.1 + debug: 3.1.0 + isarray: 2.0.1 + transitivePeerDependencies: + - supports-color + + socket.io-parser@3.4.4: + dependencies: + component-emitter: 1.2.1 + debug: 4.1.1 + isarray: 2.0.1 + transitivePeerDependencies: + - supports-color + + socket.io-parser@4.2.6: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + socket.io-redis@5.4.0: + dependencies: + debug: 4.1.1 + notepack.io: 2.2.0 + redis: 3.1.2 + socket.io-adapter: 1.1.2 + uid2: 0.0.3 + transitivePeerDependencies: + - supports-color + + socket.io@2.5.1: + dependencies: + debug: 4.1.1 + engine.io: 3.6.2 + has-binary2: 1.0.3 + socket.io-adapter: 1.1.2 + socket.io-client: 2.5.0 + socket.io-parser: 3.4.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.6 + debug: 4.4.3 + engine.io: 6.6.8 + socket.io-adapter: 2.5.7 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + sort-keys@1.1.2: + dependencies: + is-plain-obj: 1.1.0 + + sort-keys@2.0.0: + dependencies: + is-plain-obj: 1.1.0 + + sorted-array-functions@1.3.0: {} + + source-list-map@2.0.1: {} + + source-map-js@1.2.1: {} + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-support@0.4.18: + dependencies: + source-map: 0.5.7 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-url@0.4.1: {} + + source-map@0.1.43: + dependencies: + amdefine: 1.0.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + space-separated-tokens@1.1.5: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + speed-measure-webpack-plugin@1.6.0(webpack@4.47.0): + dependencies: + chalk: 4.1.2 + webpack: 4.47.0 + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + split2@2.2.0: + dependencies: + through2: 2.0.5 + + split2@3.2.2: + dependencies: + readable-stream: 3.6.2 + + split@0.3.3: + dependencies: + through: 2.3.8 + + split@1.0.1: + dependencies: + through: 2.3.8 + + sprintf-js@1.0.3: {} + + sqlstring@2.3.3: {} + + ssh2-streams@0.4.10: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + streamsearch: 0.1.2 + + ssh2@0.8.9: + dependencies: + ssh2-streams: 0.4.10 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.27.0 + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + ssri@5.3.0: + dependencies: + safe-buffer: 5.2.1 + + ssri@6.0.2: + dependencies: + figgy-pudding: 3.5.2 + + stable@0.1.8: {} + + stack-trace@0.0.10: {} + + standard-version@9.5.0: + dependencies: + chalk: 2.4.2 + conventional-changelog: 3.1.25 + conventional-changelog-config-spec: 2.1.0 + conventional-changelog-conventionalcommits: 4.6.3 + conventional-recommended-bump: 6.1.0 + detect-indent: 6.1.0 + detect-newline: 3.1.0 + dotgitignore: 2.1.0 + figures: 3.2.0 + find-up: 5.0.0 + git-semver-tags: 4.1.1 + semver: 7.8.0 + stringify-package: 1.0.1 + yargs: 16.2.0 + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + stats-webpack-plugin@0.7.0(webpack@4.47.0): + dependencies: + lodash: 4.18.1 + webpack: 4.47.0 + + statuses@1.2.1: {} + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + stealthy-require@1.1.1: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stream-browserify@2.0.2: + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + + stream-buffers@3.0.3: {} + + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + + stream-counter@0.2.0: + dependencies: + readable-stream: 1.1.14 + + stream-each@1.2.3: + dependencies: + end-of-stream: 1.4.5 + stream-shift: 1.0.3 + + stream-http@2.8.3: + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 2.3.8 + to-arraybuffer: 1.0.1 + xtend: 4.0.2 + + stream-shift@1.0.3: {} + + stream-slice@0.1.2: {} + + stream-wormhole@1.1.0: {} + + streamifier@0.1.1: {} + + streamsearch@0.1.2: {} + + strict-uri-encode@1.1.0: {} + + string-convert@0.2.1: {} + + string-width@1.0.2: + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + + string-width@2.1.1: + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + + string-width@3.1.0: + dependencies: + emoji-regex: 7.0.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 5.2.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@0.10.31: {} + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringifier@1.4.1: + dependencies: + core-js: 2.6.12 + traverse: 0.6.11 + type-name: 2.0.2 + + stringify-package@1.0.1: {} + + strip-ansi@3.0.1: + dependencies: + ansi-regex: 2.1.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@2.0.0: + dependencies: + is-utf8: 0.2.1 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-eof@1.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@1.0.1: + dependencies: + get-stdin: 4.0.1 + + strip-indent@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + style-loader@0.18.2: + dependencies: + loader-utils: 1.4.2 + schema-utils: 0.3.0 + + style-search@0.1.0: {} + + style-to-object@0.3.0: + dependencies: + inline-style-parser: 0.1.1 + + stylehacks@4.0.3: + dependencies: + browserslist: 4.28.2 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + + stylelint-config-recommended@7.0.0(stylelint@14.11.0): + dependencies: + stylelint: 14.11.0 + + stylelint-config-standard@25.0.0(stylelint@14.11.0): + dependencies: + stylelint: 14.11.0 + stylelint-config-recommended: 7.0.0(stylelint@14.11.0) + + stylelint-order@5.0.0(stylelint@14.11.0): + dependencies: + postcss: 8.4.14 + postcss-sorting: 7.0.1(postcss@8.4.14) + stylelint: 14.11.0 + + stylelint-scss@4.3.0(stylelint@14.11.0): + dependencies: + lodash: 4.18.1 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + stylelint: 14.11.0 + + stylelint@14.11.0: + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.1.2) + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 7.1.0 + css-functions-list: 3.3.3 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 6.0.1 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 5.3.2 + import-lazy: 4.0.0 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.25.0 + mathml-tag-names: 2.1.3 + meow: 9.0.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.15 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 6.0.0(postcss@8.5.15) + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + style-search: 0.1.0 + supports-hyperlinks: 2.3.0 + svg-tags: 1.0.0 + table: 6.9.0 + v8-compile-cache: 2.4.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + supports-color@2.0.0: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@6.0.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@2.3.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-tags@1.0.0: {} + + svgo@1.3.2: + dependencies: + chalk: 2.4.2 + coa: 2.0.2 + css-select: 2.1.0 + css-select-base-adapter: 0.1.1 + css-tree: 1.0.0-alpha.37 + csso: 4.2.0 + js-yaml: 3.14.2 + mkdirp: 0.5.6 + object.values: 1.2.1 + sax: 1.2.4 + stable: 0.1.8 + unquote: 1.1.1 + util.promisify: 1.0.1 + + sw-precache@5.2.1: + dependencies: + dom-urls: 1.1.0 + es6-promise: 4.2.8 + glob: 7.2.3 + lodash.defaults: 4.2.0 + lodash.template: 4.18.1 + meow: 3.7.0 + mkdirp: 0.5.6 + pretty-bytes: 4.0.2 + sw-toolbox: 3.6.0 + update-notifier: 2.5.0 + + sw-toolbox@3.6.0: + dependencies: + path-to-regexp: 1.9.0 + serviceworker-cache-polyfill: 4.0.0 + + swap-case@1.1.2: + dependencies: + lower-case: 1.1.4 + upper-case: 1.1.3 + + symbol-tree@3.2.4: {} + + table@4.0.2: + dependencies: + ajv: 5.5.2 + ajv-keywords: 2.1.1(ajv@5.5.2) + chalk: 2.4.2 + lodash: 4.18.1 + slice-ansi: 1.0.0 + string-width: 2.1.1 + + table@6.9.0: + dependencies: + ajv: 8.20.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tapable@0.2.9: {} + + tapable@1.1.3: {} + + tar-stream@1.6.2: + dependencies: + bl: 1.2.3 + buffer-alloc: 1.2.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + readable-stream: 2.3.8 + to-buffer: 1.2.2 + xtend: 4.0.2 + + tar@2.2.2: + dependencies: + block-stream: 0.0.9 + fstream: 1.0.12 + inherits: 2.0.4 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tcp-base@3.2.0: + dependencies: + is-type-of: 1.4.0 + sdk-base: 3.6.0 + + tcp-proxy.js@1.5.0: + dependencies: + debug: 3.2.7 + through2: 2.0.5 + transitivePeerDependencies: + - supports-color + + temp-dir@2.0.0: {} + + tempfile@3.0.0: + dependencies: + temp-dir: 2.0.0 + uuid: 3.4.0 + + term-size@1.2.0: + dependencies: + execa: 0.7.0 + + term-size@2.2.1: {} + + terraformer-wkt-parser@1.2.1: + dependencies: + '@types/geojson': 1.0.6 + terraformer: 1.0.12 + + terraformer@1.0.12: + optionalDependencies: + '@types/geojson': 1.0.6 + + terser-webpack-plugin@1.4.6(webpack@4.28.4): + dependencies: + cacache: 12.0.4 + find-cache-dir: 2.1.0 + is-wsl: 1.1.0 + schema-utils: 1.0.0 + serialize-javascript: 4.0.0 + source-map: 0.6.1 + terser: 4.8.1 + webpack: 4.28.4 + webpack-sources: 1.4.3 + worker-farm: 1.7.0 + + terser-webpack-plugin@1.4.6(webpack@4.47.0): + dependencies: + cacache: 12.0.4 + find-cache-dir: 2.1.0 + is-wsl: 1.1.0 + schema-utils: 1.0.0 + serialize-javascript: 4.0.0 + source-map: 0.6.1 + terser: 4.8.1 + webpack: 4.47.0 + webpack-sources: 1.4.3 + worker-farm: 1.7.0 + + terser@4.8.1: + dependencies: + acorn: 8.16.0 + commander: 2.20.3 + source-map: 0.6.1 + source-map-support: 0.5.21 + + test-exclude@5.2.3: + dependencies: + glob: 7.2.3 + minimatch: 3.1.5 + read-pkg-up: 4.0.0 + require-main-filename: 2.0.0 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.3 + minimatch: 3.1.5 + + text-extensions@1.9.0: {} + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-loader@1.2.0(webpack@4.47.0): + dependencies: + async: 2.6.4 + loader-runner: 2.4.0 + loader-utils: 1.4.2 + webpack: 4.47.0 + + throat@5.0.0: {} + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + through@2.3.8: {} + + timed-out@4.0.1: {} + + timers-browserify@2.0.12: + dependencies: + setimmediate: 1.0.5 + + timers-ext@0.1.8: + dependencies: + es5-ext: 0.10.64 + next-tick: 1.1.0 + + timsort@0.3.0: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinycolor2@1.6.0: {} + + tinydate@1.3.0: {} + + title-case@2.1.1: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-array@0.1.4: {} + + to-arraybuffer@1.0.1: {} + + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + + to-fast-properties@1.0.3: {} + + to-fast-properties@2.0.0: {} + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-readable-stream@1.0.0: {} + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + toggle-selection@1.0.6: {} + + toidentifier@1.0.1: {} + + toposort-class@1.0.1: {} + + toposort@1.0.7: {} + + tough-cookie@2.5.0: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + + tough-cookie@3.0.1: + dependencies: + ip-regex: 2.1.0 + psl: 1.15.0 + punycode: 2.3.1 + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tr46@2.1.0: + dependencies: + punycode: 2.3.1 + + traverse@0.6.11: + dependencies: + gopd: 1.2.0 + typedarray.prototype.slice: 1.0.5 + which-typed-array: 1.1.20 + + trim-newlines@1.0.0: {} + + trim-newlines@2.0.0: {} + + trim-newlines@3.0.1: {} + + trim-right@1.0.1: {} + + trough@1.0.5: {} + + tryer@1.0.1: {} + + ts-loader@8.4.0(typescript@4.7.4)(webpack@4.47.0): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 4.5.0 + loader-utils: 2.0.4 + micromatch: 4.0.8 + semver: 7.8.0 + typescript: 4.7.4 + webpack: 4.47.0 + + ts-node@10.9.2(@types/node@25.9.1)(typescript@4.7.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 25.9.1 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 4.7.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + ts-node@7.0.1: + dependencies: + arrify: 1.0.1 + buffer-from: 1.1.2 + diff: 3.5.1 + make-error: 1.3.6 + minimist: 1.2.8 + mkdirp: 0.5.6 + source-map-support: 0.5.21 + yn: 2.0.0 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsscmp@1.0.5: {} + + tsscmp@1.0.6: {} + + tsutils@3.21.0(typescript@4.7.4): + dependencies: + tslib: 1.14.1 + typescript: 4.7.4 + + tty-browserify@0.0.0: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + + tweezer.js@1.5.0: {} + + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.18.1: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.6.0: {} + + type-fest@0.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + type-name@2.0.2: {} + + type@2.7.3: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typedarray.prototype.slice@1.0.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-proto: 1.0.1 + math-intrinsics: 1.1.0 + typed-array-buffer: 1.0.3 + typed-array-byte-offset: 1.0.4 + + typedarray@0.0.6: {} + + typescript@4.7.4: {} + + ua-parser-js@0.7.41: {} + + uglify-es@3.3.9: + dependencies: + commander: 2.13.0 + source-map: 0.6.1 + + uglify-js@3.19.3: {} + + uglify-js@3.4.10: + dependencies: + commander: 2.19.0 + source-map: 0.6.1 + + uglifyjs-webpack-plugin@2.2.0(webpack@4.47.0): + dependencies: + cacache: 12.0.4 + find-cache-dir: 2.1.0 + is-wsl: 1.1.0 + schema-utils: 1.0.0 + serialize-javascript: 1.9.1 + source-map: 0.6.1 + uglify-js: 3.19.3 + webpack: 4.47.0 + webpack-sources: 1.4.3 + worker-farm: 1.7.0 + + uid-safe@2.0.0: + dependencies: + base64-url: 1.2.1 + + uid-safe@2.1.4: + dependencies: + random-bytes: 1.0.0 + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + uid2@0.0.3: {} + + ultron@1.0.2: {} + + umzug@2.3.0: + dependencies: + bluebird: 3.7.2 + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@5.26.5: {} + + undici-types@7.24.6: {} + + undici@7.25.0: {} + + unescape@1.0.1: + dependencies: + extend-shallow: 2.0.1 + + unified@9.2.2: + dependencies: + '@types/unist': 2.0.11 + bail: 1.0.5 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 2.1.0 + trough: 1.0.5 + vfile: 4.2.1 + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + uniq@1.0.1: {} + + uniqs@2.0.0: {} + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + + unique-string@1.0.0: + dependencies: + crypto-random-string: 1.0.0 + + unique-string@2.0.0: + dependencies: + crypto-random-string: 2.0.0 + + unist-builder@2.0.3: {} + + unist-util-generated@1.1.6: {} + + unist-util-is@4.1.0: {} + + unist-util-position@3.1.0: {} + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + universal-deep-strict-equal@1.2.2: + dependencies: + array-filter: 1.0.0 + indexof: 0.0.1 + object-keys: 1.1.1 + + universalify@0.1.2: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unquote@1.1.1: {} + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + unzip-response@2.0.1: {} + + upath@1.2.0: + optional: true + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-notifier@2.5.0: + dependencies: + boxen: 1.3.0 + chalk: 2.4.2 + configstore: 3.1.5 + import-lazy: 2.1.0 + is-ci: 1.2.1 + is-installed-globally: 0.1.0 + is-npm: 1.0.0 + latest-version: 3.1.0 + semver-diff: 2.1.0 + xdg-basedir: 3.0.0 + + update-notifier@4.1.3: + dependencies: + boxen: 4.2.0 + chalk: 3.0.0 + configstore: 5.0.1 + has-yarn: 2.1.0 + import-lazy: 2.1.0 + is-ci: 2.0.0 + is-installed-globally: 0.3.2 + is-npm: 4.0.0 + is-yarn-global: 0.3.0 + latest-version: 5.1.0 + pupa: 2.1.1 + semver-diff: 3.1.1 + xdg-basedir: 4.0.0 + + upper-case-first@1.1.2: + dependencies: + upper-case: 1.1.3 + + upper-case@1.1.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urijs@1.19.11: {} + + urix@0.1.0: {} + + url-loader@0.5.9(file-loader@1.1.11(webpack@4.47.0)): + dependencies: + file-loader: 1.1.11(webpack@4.47.0) + loader-utils: 1.4.2 + mime: 1.3.6 + + url-loader@1.1.2(webpack@4.47.0): + dependencies: + loader-utils: 1.4.2 + mime: 2.6.0 + schema-utils: 1.0.0 + webpack: 4.47.0 + + url-parse-lax@1.0.0: + dependencies: + prepend-http: 1.0.4 + + url-parse-lax@3.0.0: + dependencies: + prepend-http: 2.0.0 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.15.2 + + urllib@2.44.0: + dependencies: + any-promise: 1.3.0 + content-type: 1.0.5 + default-user-agent: 1.0.0 + digest-header: 1.1.0 + ee-first: 1.1.1 + formstream: 1.5.2 + humanize-ms: 1.2.1 + iconv-lite: 0.6.3 + pump: 3.0.4 + qs: 6.15.2 + statuses: 1.5.0 + utility: 1.18.0 + + use@3.1.1: {} + + utf8@3.0.0: {} + + util-deprecate@1.0.2: {} + + util.promisify@1.0.0: + dependencies: + define-properties: 1.2.1 + object.getownpropertydescriptors: 2.1.9 + + util.promisify@1.0.1: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + has-symbols: 1.1.0 + object.getownpropertydescriptors: 2.1.9 + + util@0.10.4: + dependencies: + inherits: 2.0.3 + + util@0.11.1: + dependencies: + inherits: 2.0.3 + + utila@0.4.0: {} + + utility@1.18.0: + dependencies: + copy-to: 2.0.1 + escape-html: 1.0.3 + mkdirp: 0.5.6 + mz: 2.7.0 + unescape: 1.0.1 + + utility@2.5.0: + dependencies: + escape-html: 1.0.3 + unescape: 1.0.1 + ylru: 2.0.0 + + utils-merge@1.0.0: {} + + utils-merge@1.0.1: {} + + uuid@3.4.0: {} + + uuid@8.3.2: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-compile-cache@2.4.0: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validator@10.11.0: {} + + value-equal@1.0.1: {} + + vary@1.0.1: {} + + vary@1.1.2: {} + + vconsole-webpack-plugin@1.8.0: + dependencies: + resize-observer-polyfill: 1.5.1 + vconsole: 3.15.1 + + vconsole@3.15.1: + dependencies: + '@babel/runtime': 7.29.2 + copy-text-to-clipboard: 3.2.2 + core-js: 3.49.0 + mutation-observer: 1.0.3 + + vendors@1.0.4: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + + vfile@4.2.1: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 2.0.3 + vfile-message: 2.0.4 + + vhost@3.0.2: {} + + vm-browserify@1.1.2: {} + + vue-cli-plugin-commitlint@1.0.12: {} + + w3c-hr-time@1.0.2: + dependencies: + browser-process-hrtime: 1.0.0 + + w3c-xmlserializer@1.1.2: + dependencies: + domexception: 1.0.1 + webidl-conversions: 4.0.2 + xml-name-validator: 3.0.0 + + w3c-xmlserializer@2.0.0: + dependencies: + xml-name-validator: 3.0.0 + + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + watchpack-chokidar2@2.0.1: + dependencies: + chokidar: 2.1.8 + transitivePeerDependencies: + - supports-color + optional: true + + watchpack@1.7.5: + dependencies: + graceful-fs: 4.2.11 + neo-async: 2.6.2 + optionalDependencies: + chokidar: 3.6.0 + watchpack-chokidar2: 2.0.1 + transitivePeerDependencies: + - supports-color + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webidl-conversions@4.0.2: {} + + webidl-conversions@5.0.0: {} + + webidl-conversions@6.1.0: {} + + webpack-asset-file-plugin@1.0.2: {} + + webpack-bundle-analyzer@3.9.0: + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + bfj: 6.1.2 + chalk: 2.4.2 + commander: 2.20.3 + ejs: 2.7.4 + express: 4.22.2 + filesize: 3.6.1 + gzip-size: 5.1.1 + lodash: 4.18.1 + mkdirp: 0.5.6 + opener: 1.5.2 + ws: 6.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + webpack-dev-middleware@3.6.0(webpack@4.28.4): + dependencies: + memory-fs: 0.4.1 + mime: 2.6.0 + range-parser: 1.2.1 + webpack: 4.28.4 + webpack-log: 2.0.0 + + webpack-filter-warnings-plugin@1.2.1(webpack@4.47.0): + dependencies: + webpack: 4.47.0 + + webpack-hot-middleware@2.26.1: + dependencies: + ansi-html-community: 0.0.8 + html-entities: 2.6.0 + strip-ansi: 6.0.1 + + webpack-log@2.0.0: + dependencies: + ansi-colors: 3.2.4 + uuid: 3.4.0 + + webpack-manifest-resource-plugin@4.2.7: + dependencies: + fs-extra: 0.30.0 + lodash: 4.18.1 + + webpack-merge@4.2.2: + dependencies: + lodash: 4.18.1 + + webpack-node-externals@1.7.2: {} + + webpack-sources@1.4.3: + dependencies: + source-list-map: 2.0.1 + source-map: 0.6.1 + + webpack-tool@4.5.4: + dependencies: + '@easy-team/koa-history-api-fallback': 1.0.0 + art-template: 4.13.4 + chalk: 2.4.2 + detect-port: 1.6.1 + easy-helper: 1.0.2 + http-proxy: 1.18.1 + http-proxy-middleware: 0.20.0 + kcors: 1.3.3 + koa: 1.7.1 + koa-connect: 1.0.0 + koa-webpack-hot-middleware: 1.0.3 + node-tool-utils: 1.6.0 + opn: 5.5.0 + webpack: 4.28.4 + webpack-dev-middleware: 3.6.0(webpack@4.28.4) + webpack-merge: 4.2.2 + transitivePeerDependencies: + - debug + - supports-color + - webpack-cli + - webpack-command + + webpack@4.28.4: + dependencies: + '@webassemblyjs/ast': 1.7.11 + '@webassemblyjs/helper-module-context': 1.7.11 + '@webassemblyjs/wasm-edit': 1.7.11 + '@webassemblyjs/wasm-parser': 1.7.11 + acorn: 5.7.4 + acorn-dynamic-import: 3.0.0 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + chrome-trace-event: 1.0.4 + enhanced-resolve: 4.5.0 + eslint-scope: 4.0.3 + json-parse-better-errors: 1.0.2 + loader-runner: 2.4.0 + loader-utils: 1.4.2 + memory-fs: 0.4.1 + micromatch: 3.1.10 + mkdirp: 0.5.6 + neo-async: 2.6.2 + node-libs-browser: 2.2.1 + schema-utils: 0.4.7 + tapable: 1.1.3 + terser-webpack-plugin: 1.4.6(webpack@4.28.4) + watchpack: 1.7.5 + webpack-sources: 1.4.3 + transitivePeerDependencies: + - supports-color + + webpack@4.47.0: + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-module-context': 1.9.0 + '@webassemblyjs/wasm-edit': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + acorn: 6.4.2 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + chrome-trace-event: 1.0.4 + enhanced-resolve: 4.5.0 + eslint-scope: 4.0.3 + json-parse-better-errors: 1.0.2 + loader-runner: 2.4.0 + loader-utils: 1.4.2 + memory-fs: 0.4.1 + micromatch: 3.1.10 + mkdirp: 0.5.6 + neo-async: 2.6.2 + node-libs-browser: 2.2.1 + schema-utils: 1.0.0 + tapable: 1.1.3 + terser-webpack-plugin: 1.4.6(webpack@4.47.0) + watchpack: 1.7.5 + webpack-sources: 1.4.3 + transitivePeerDependencies: + - supports-color + + whatwg-encoding@1.0.5: + dependencies: + iconv-lite: 0.4.24 + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-fetch@3.6.20: {} + + whatwg-mimetype@2.3.0: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + whatwg-url@8.7.0: + dependencies: + lodash: 4.18.1 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@1.0.0: {} + + which-module@2.0.1: {} + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wide-align@1.1.3: + dependencies: + string-width: 2.1.1 + + widest-line@2.0.1: + dependencies: + string-width: 2.1.1 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + win-release@1.1.1: + dependencies: + semver: 5.7.2 + + wkx@0.4.8: + dependencies: + '@types/node': 25.9.1 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + worker-farm@1.7.0: + dependencies: + errno: 0.1.8 + + wrap-ansi@2.1.0: + dependencies: + string-width: 1.0.2 + strip-ansi: 3.0.1 + + wrap-ansi@5.1.0: + dependencies: + ansi-styles: 3.2.1 + string-width: 3.1.0 + strip-ansi: 5.2.0 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@2.4.3: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-json-file@2.3.0: + dependencies: + detect-indent: 5.0.0 + graceful-fs: 4.2.11 + make-dir: 1.3.0 + pify: 3.0.0 + sort-keys: 2.0.0 + write-file-atomic: 2.4.3 + + write-json-file@3.2.0: + dependencies: + detect-indent: 5.0.0 + graceful-fs: 4.2.11 + make-dir: 2.1.0 + pify: 4.0.1 + sort-keys: 2.0.0 + write-file-atomic: 2.4.3 + + write-pkg@3.2.0: + dependencies: + sort-keys: 2.0.0 + write-json-file: 2.3.0 + + write@0.2.1: + dependencies: + mkdirp: 0.5.6 + + write@1.0.3: + dependencies: + mkdirp: 0.5.6 + + ws@1.1.1: + dependencies: + options: 0.0.6 + ultron: 1.0.2 + + ws@6.2.3: + dependencies: + async-limiter: 1.0.1 + + ws@7.5.10: {} + + ws@8.20.1: {} + + wt@1.2.0: + dependencies: + debug: 2.6.9 + ndir: 0.1.5 + sdk-base: 2.0.1 + transitivePeerDependencies: + - supports-color + + wtf-8@1.0.0: {} + + xdg-basedir@3.0.0: {} + + xdg-basedir@4.0.0: {} + + xml-name-validator@3.0.0: {} + + xmlchars@2.2.0: {} + + xmlhttprequest-ssl@1.5.3: {} + + xmlhttprequest-ssl@1.6.3: {} + + xss@1.0.15: + dependencies: + commander: 2.20.3 + cssfilter: 0.0.10 + + xtend@4.0.2: {} + + xterm-addon-attach@0.6.0(xterm@4.19.0): + dependencies: + xterm: 4.19.0 + + xterm-addon-fit@0.5.0(xterm@4.19.0): + dependencies: + xterm: 4.19.0 + + xterm@4.19.0: {} + + y18n@3.2.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@2.1.2: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@1.10.3: {} + + yargonaut@1.1.4: + dependencies: + chalk: 1.1.3 + figlet: 1.11.0 + parent-require: 1.0.0 + + yargs-parser@10.1.0: + dependencies: + camelcase: 4.1.0 + + yargs-parser@13.1.2: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.9: {} + + yargs-parser@5.0.1: + dependencies: + camelcase: 3.0.0 + object.assign: 4.1.7 + + yargs-unparser@1.6.0: + dependencies: + flat: 4.1.1 + lodash: 4.18.1 + yargs: 13.3.2 + + yargs@13.3.2: + dependencies: + cliui: 5.0.0 + find-up: 3.0.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 3.1.0 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 13.1.2 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@7.1.2: + dependencies: + camelcase: 3.0.0 + cliui: 3.2.0 + decamelize: 1.2.0 + get-caller-file: 1.0.3 + os-locale: 1.4.0 + read-pkg-up: 1.0.1 + require-directory: 2.1.1 + require-main-filename: 1.0.1 + set-blocking: 2.0.0 + string-width: 1.0.2 + which-module: 1.0.0 + y18n: 3.2.2 + yargs-parser: 5.0.1 + + yazl@2.5.1: + dependencies: + buffer-crc32: 0.2.13 + + yeast@0.1.2: {} + + ylru@1.4.0: {} + + ylru@2.0.0: {} + + yn@2.0.0: {} + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + ypkgfiles@1.6.0: + dependencies: + debug: 2.6.9 + glob: 7.2.3 + is-type-of: 1.4.0 + resolve-files: 1.0.2 + yargs: 7.1.2 + transitivePeerDependencies: + - supports-color + + zip-stream@1.2.0: + dependencies: + archiver-utils: 1.3.0 + compress-commons: 1.2.2 + lodash: 4.18.1 + readable-stream: 2.3.8 + + zlogger@1.1.0: + dependencies: + pumpify: 1.5.1 + split2: 2.2.0 + through2: 2.0.5 + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} + + zwitch@1.0.5: {} diff --git a/test/clawhub-contract.test.js b/test/clawhub-contract.test.js new file mode 100644 index 00000000..563553a2 --- /dev/null +++ b/test/clawhub-contract.test.js @@ -0,0 +1,874 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const skillFingerprint = require('../contracts/skill-fingerprint'); +const goldenVectors = require('../contracts/skill-fingerprint/golden-vectors.v1.json'); + +// Mock app and ctx helpers +function createMockApp(models = {}) { + return { + model: models, + Sequelize: { + Op: { + like: 'like', + or: 'or', + lt: 'lt', + eq: 'eq', + in: 'in', + ne: 'ne', + }, + literal: (v) => v, + }, + utils: { + response: (success, data) => ({ success, data }), + }, + }; +} + +function createMockCtx(query = {}, params = {}, body = {}, files = []) { + return { + query, + params, + request: { body, files, headers: {} }, + throw(status, message) { + const err = new Error(message); + err.status = status; + throw err; + }, + logger: { warn: () => {}, info: () => {} }, + set: () => {}, + status: 200, + body: null, + }; +} + +// Load the service module +const ClawhubService = require('../app/service/clawhub'); + +// ============================================================ +// Phase 2: Foundation Tests (T005) +// ============================================================ + +test('ClawhubService can be instantiated with mock app and ctx', () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp(); + service.ctx = createMockCtx(); + assert.equal(typeof service.getRegistryMetadata, 'function'); +}); + +// ============================================================ +// Phase 3: US1 Discovery Tests (T006-T011) +// ============================================================ + +test('getRegistryMetadata returns well-known config', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp(); + service.ctx = createMockCtx(); + + const meta = await service.getRegistryMetadata('http://10.0.0.8:7001'); + assert.equal(meta.apiBase, 'http://10.0.0.8:7001'); + assert.equal(meta.authBase, null); + assert.equal(meta.minCliVersion, '0.9.0'); +}); + +test('searchSkills returns flat results with score 1.0', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findAll: async () => [ + { + slug: 'react-skill', + name: 'React Skill', + description: 'React description', + version: '1.0.0', + stars: 5, + updated_at: new Date('2026-05-21T10:00:00Z'), + }, + ], + }, + }); + service.ctx = createMockCtx(); + + const results = await service.searchSkills('react', 10); + assert.equal(results.length, 1); + assert.equal(results[0].slug, 'react-skill'); + assert.equal(results[0].displayName, 'React Skill'); + assert.equal(results[0].summary, 'React description'); + assert.equal(results[0].version, '1.0.0'); + assert.equal(results[0].score, 1.0); + assert.equal(results[0].updatedAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.equal(results[0].ownerHandle, null); + assert.equal(results[0].owner, null); +}); + +test('searchSkills with empty query returns all skills', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findAll: async () => [], + }, + }); + service.ctx = createMockCtx(); + + const results = await service.searchSkills('', 10); + assert.equal(results.length, 0); +}); + +test('listSkills returns items with cursor pagination', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findAll: async () => [ + { + id: 2, + slug: 'skill-b', + name: 'Skill B', + description: 'Desc B', + version: '1.0.0', + tags: '["test"]', + stars: 3, + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }, + { + id: 1, + slug: 'skill-a', + name: 'Skill A', + description: 'Desc A', + version: '2.0.0', + tags: '[]', + stars: 10, + created_at: new Date('2026-05-21T09:00:00Z'), + updated_at: new Date('2026-05-21T09:00:00Z'), + }, + ], + }, + }); + service.ctx = createMockCtx(); + + const data = await service.listSkills(null, 'stars', 2); + assert.equal(data.items.length, 2); + assert.ok(Array.isArray(data.items)); + assert.ok('nextCursor' in data); + + const first = data.items[0]; + assert.equal(first.slug, 'skill-b'); + assert.equal(first.displayName, 'Skill B'); + assert.equal(first.summary, 'Desc B'); + assert.deepEqual(first.tags, ['test']); + assert.deepEqual(first.stats, { stars: 3, downloads: 0 }); + assert.equal(first.createdAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.equal(first.updatedAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.deepEqual(first.latestVersion, { + version: '1.0.0', + createdAt: new Date('2026-05-21T10:00:00Z').getTime(), + changelog: '', + license: null, + }); +}); + +test('listSkills uses a composite cursor that matches newest sorting', async () => { + const calls = []; + const rows = [ + { + id: 9, + slug: 'skill-b', + name: 'Skill B', + tags: '[]', + stars: 2, + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }, + { + id: 8, + slug: 'skill-a', + name: 'Skill A', + tags: '[]', + stars: 1, + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }, + ]; + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findAll: async (options) => { + calls.push(options); + return rows; + }, + }, + }); + service.ctx = createMockCtx(); + service.ctx.service = { skills: { ensureStorageReady: async () => {} } }; + + const firstPage = await service.listSkills(null, 'newest', 1); + await service.listSkills(firstPage.nextCursor, 'newest', 1); + + assert.deepEqual(calls[0].order, [ + ['updated_at', 'DESC'], + ['id', 'DESC'], + ]); + assert.deepEqual(calls[1].where.or, [ + { updated_at: { lt: new Date('2026-05-21T10:00:00Z') } }, + { + updated_at: new Date('2026-05-21T10:00:00Z'), + id: { lt: 9 }, + }, + ]); +}); + +test('listSkills uses a composite cursor that matches stars sorting', async () => { + const calls = []; + const rows = [ + { + id: 7, + slug: 'skill-b', + name: 'Skill B', + tags: '[]', + stars: 12, + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }, + { + id: 6, + slug: 'skill-a', + name: 'Skill A', + tags: '[]', + stars: 12, + created_at: new Date('2026-05-21T09:00:00Z'), + updated_at: new Date('2026-05-21T09:00:00Z'), + }, + ]; + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findAll: async (options) => { + calls.push(options); + return rows; + }, + }, + }); + service.ctx = createMockCtx(); + service.ctx.service = { skills: { ensureStorageReady: async () => {} } }; + + const firstPage = await service.listSkills(null, 'stars', 1); + await service.listSkills(firstPage.nextCursor, 'stars', 1); + + assert.deepEqual(calls[0].order, [ + ['stars', 'DESC'], + ['id', 'DESC'], + ]); + assert.deepEqual(calls[1].where.or, [ + { stars: { lt: 12 } }, + { + stars: 12, + id: { lt: 7 }, + }, + ]); +}); + +test('searchSkills ensures the skills schema is ready before querying', async () => { + const events = []; + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findAll: async () => { + events.push('query'); + return []; + }, + }, + }); + service.ctx = createMockCtx(); + service.ctx.service = { + skills: { + ensureStorageReady: async () => { + events.push('migrate'); + }, + }, + }; + + await service.searchSkills('demo', 10); + + assert.deepEqual(events, ['migrate', 'query']); +}); + +test('getSkillDetail returns full skill object', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ + slug: 'my-skill', + name: 'My Skill', + description: 'A test skill', + version: '1.2.3', + tags: '["test"]', + stars: 42, + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }), + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillDetail('my-skill'); + assert.ok(data); + assert.equal(data.skill.slug, 'my-skill'); + assert.equal(data.skill.displayName, 'My Skill'); + assert.equal(data.skill.summary, 'A test skill'); + assert.equal(data.skill.version, '1.2.3'); + assert.deepEqual(data.skill.tags, ['test']); + assert.deepEqual(data.skill.stats, { stars: 42, downloads: 0 }); + assert.equal(data.skill.createdAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.equal(data.skill.updatedAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.deepEqual(data.latestVersion, { + version: '1.2.3', + createdAt: new Date('2026-05-21T10:00:00Z').getTime(), + changelog: '', + license: null, + }); + assert.equal(data.owner, null); + assert.equal(data.moderation, null); +}); + +test('getSkillDetail returns null for missing skill', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => null, + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillDetail('nonexistent'); + assert.equal(data, null); +}); + +test('getSkillDetail returns children for a package skill', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ + slug: 'my-pack', + name: 'My Pack', + description: 'A package', + version: '1.0.0', + tags: '[]', + stars: 5, + is_package: 1, + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }), + findAll: async (options) => { + if (options.where.parent_slug === 'my-pack') { + return [ + { + id: 2, + slug: 'sub-a', + name: 'Sub A', + description: 'Sub desc', + version: '1.0.0', + tags: '[]', + stars: 3, + is_package: 0, + parent_slug: 'my-pack', + created_at: new Date('2026-05-21T10:00:00Z'), + updated_at: new Date('2026-05-21T10:00:00Z'), + }, + ]; + } + return []; + }, + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillDetail('my-pack'); + assert.ok(data); + assert.equal(data.skill.isPackage, true); + assert.equal(data.skill.children.length, 1); + assert.equal(data.skill.children[0].slug, 'sub-a'); + assert.equal(data.skill.children[0].displayName, 'Sub A'); +}); + +test('listSkillVersions returns single version list', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ + slug: 'my-skill', + version: '1.0.0', + updated_at: new Date('2026-05-21T10:00:00Z'), + }), + }, + }); + service.ctx = createMockCtx(); + + const data = await service.listSkillVersions('my-skill'); + assert.ok(data); + assert.equal(data.items.length, 1); + assert.equal(data.items[0].version, '1.0.0'); + assert.equal(data.items[0].createdAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.equal(data.items[0].changelog, ''); + assert.equal(data.items[0].changelogSource, null); + assert.equal(data.nextCursor, null); +}); + +test('getSkillVersionDetail returns version for matching current version', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ + slug: 'my-skill', + name: 'My Skill', + version: '1.0.0', + updated_at: new Date('2026-05-21T10:00:00Z'), + }), + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillVersionDetail('my-skill', '1.0.0'); + assert.ok(data); + assert.equal(data.version.version, '1.0.0'); + assert.equal(data.version.createdAt, new Date('2026-05-21T10:00:00Z').getTime()); + assert.equal(data.version.changelog, ''); + assert.equal(data.version.changelogSource, null); + assert.equal(data.version.license, null); + assert.equal(data.skill.slug, 'my-skill'); + assert.equal(data.skill.displayName, 'My Skill'); +}); + +test('getSkillVersionDetail returns null for non-matching version', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ + slug: 'my-skill', + name: 'My Skill', + version: '1.0.0', + updated_at: new Date(), + }), + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillVersionDetail('my-skill', '2.0.0'); + assert.equal(data, null); +}); + +// ============================================================ +// Phase 4: US2 Download Tests (T019-T020) +// ============================================================ + +test('getSkillFileContent returns file content', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ id: 1, slug: 'my-skill' }), + }, + SkillsFile: { + findOne: async () => ({ + file_path: 'SKILL.md', + content: '# Hello', + is_binary: 0, + }), + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillFileContent('my-skill', 'SKILL.md'); + assert.ok(data); + assert.equal(data.content, '# Hello'); + assert.equal(data.path, 'SKILL.md'); +}); + +test('getSkillFileContent returns null for missing file', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ id: 1, slug: 'my-skill' }), + }, + SkillsFile: { + findOne: async () => null, + }, + }); + service.ctx = createMockCtx(); + + const data = await service.getSkillFileContent('my-skill', 'MISSING.md'); + assert.equal(data, null); +}); + +test('buildSkillZip returns zip buffer', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ id: 1, slug: 'my-skill', version: '1.0.0' }), + }, + SkillsFile: { + findAll: async () => [{ file_path: 'SKILL.md', content: '# Hello', is_binary: 0 }], + }, + }); + service.ctx = createMockCtx(); + + const result = await service.buildSkillZip('my-skill'); + assert.ok(result); + assert.ok(result.fileName.includes('my-skill')); + assert.ok(Buffer.isBuffer(result.content)); + assert.ok(result.content.length > 0); +}); + +test('buildSkillZip packages skill package nested structure when is_package is 1', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async (options) => { + if (options.where.slug === 'my-skills-pack') { + return { id: 100, slug: 'my-skills-pack', is_package: 1, version: '1.0.0' }; + } + return null; + }, + findAll: async (options) => { + if (options.where.parent_slug === 'my-skills-pack') { + return [ + { id: 101, name: 'sub-skill-1', slug: 'sub-1' }, + { id: 102, name: 'sub-skill-2', slug: 'sub-2' }, + ]; + } + return []; + }, + }, + SkillsFile: { + findAll: async (options) => { + if (options.where.skill_id === 101) { + return [ + { file_path: 'README.md', content: 'c3ViLTE=', is_binary: 0 }, + ]; + } + if (options.where.skill_id === 102) { + return [ + { file_path: 'index.js', content: 'c3ViLTI=', is_binary: 0 }, + ]; + } + return []; + }, + }, + }); + service.ctx = createMockCtx(); + + const result = await service.buildSkillZip('my-skills-pack'); + assert.ok(result); + assert.ok(result.fileName.includes('my-skills-pack')); + assert.ok(Buffer.isBuffer(result.content)); + + const AdmZip = require('adm-zip'); + const zip = new AdmZip(result.content); + const entries = zip.getEntries().map(e => e.entryName); + + assert.ok(entries.includes('my-skills-pack/sub-skill-1/README.md')); + assert.ok(entries.includes('my-skills-pack/sub-skill-2/index.js')); +}); + +// ============================================================ +// Phase 5: US3 Publish Tests (T024) +// ============================================================ + +test('validateSemVer accepts valid versions', () => { + const service = Object.create(ClawhubService.prototype); + assert.equal(service.validateSemVer('1.0.0'), true); + assert.equal(service.validateSemVer('0.1.0'), true); + assert.equal(service.validateSemVer('1.2.3-alpha'), true); + assert.equal(service.validateSemVer('1.2.3+build.1'), true); +}); + +test('validateSemVer rejects invalid versions', () => { + const service = Object.create(ClawhubService.prototype); + assert.equal(service.validateSemVer('v1.0.0'), false); + assert.equal(service.validateSemVer('1.0'), false); + assert.equal(service.validateSemVer('1.0.0.0'), false); + assert.equal(service.validateSemVer(''), false); +}); + +test('isBinaryBuffer detects invalid UTF-8 content', () => { + const service = Object.create(ClawhubService.prototype); + + assert.equal(service.isBinaryBuffer(Buffer.from('valid utf8', 'utf8')), false); + assert.equal(service.isBinaryBuffer(Buffer.from([0xff, 0xfe, 0xfd])), true); + assert.equal(service.isBinaryBuffer(Buffer.from([0x61, 0x00, 0x62])), true); +}); + +test('publishSkill rejects missing SKILL.md', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { findOne: async () => null }, + SkillsSource: { findOne: async () => null, create: async () => ({ id: 1 }) }, + }); + service.ctx = createMockCtx(); + service.ctx.throw = (status, message) => { + const err = new Error(message); + err.status = status; + throw err; + }; + + await assert.rejects( + () => + service.publishSkill({ slug: 'test', displayName: 'Test', version: '1.0.0' }, [ + { filepath: 'readme.md', content: 'hi' }, + ]), + /SKILL.md/ + ); +}); + +test('publishSkill returns ok: true and string skillId and versionId', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => null, + create: async (data) => ({ id: 123, ...data, update: async () => {} }), + }, + SkillsSource: { + findOne: async () => ({ id: 1 }), + }, + SkillsFile: { + create: async () => ({}), + }, + }); + service.ctx = createMockCtx(); + + const result = await service.publishSkill( + { slug: 'test-skill', displayName: 'Test Skill', version: '1.0.0' }, + [ + { filepath: 'SKILL.md', content: 'test content' }, + ] + ); + + assert.equal(result.ok, true); + assert.equal(typeof result.skillId, 'string'); + assert.equal(result.versionId, 'v1.0.0'); +}); + +// ============================================================ +// Phase 6: US4 Resolve Tests (T028) +// ============================================================ + +test('computeSkillFingerprint returns consistent hex string', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsFile: { + findAll: async () => [ + { file_path: 'a.md', content: 'hello', is_binary: 0 }, + { file_path: 'b.md', content: 'world', is_binary: 0 }, + ], + }, + }); + service.ctx = createMockCtx(); + + const fp1 = await service.computeSkillFingerprint(1); + const fp2 = await service.computeSkillFingerprint(1); + assert.equal(typeof fp1, 'string'); + assert.equal(fp1.length, 64); + assert.equal(fp1, fp2); +}); + +test('computeSkillFingerprint matches shared golden vectors', async () => { + const testCase = goldenVectors.cases.find((entry) => entry.name === 'basic-markdown-pair'); + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsFile: { + findAll: async () => [ + ...(testCase.ignoreFiles || []).map((file) => ({ + file_path: file.path, + content: file.content, + is_binary: 0, + })), + ...testCase.files.map((file) => ({ + file_path: file.path, + content: file.content, + is_binary: file.isBinary ? 1 : 0, + })), + ], + }, + }); + service.ctx = createMockCtx(); + + const fingerprint = await service.computeSkillFingerprint(1); + assert.equal(fingerprint, testCase.fingerprint); +}); + +test('computeSkillFingerprint uses the same text-file set as the CLI', async () => { + const testCase = goldenVectors.cases.find((entry) => entry.name === 'text-file-set-filtering'); + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsFile: { + findAll: async () => [ + ...(testCase.ignoreFiles || []).map((file) => ({ + file_path: file.path, + content: file.content, + is_binary: 0, + })), + ...testCase.files.map((file) => ({ + file_path: file.path, + content: file.content, + is_binary: file.isBinary ? 1 : 0, + })), + ], + }, + }); + service.ctx = createMockCtx(); + + const fingerprint = await service.computeSkillFingerprint(1); + assert.equal(fingerprint, skillFingerprint.fingerprintFromGoldenCase(testCase)); +}); + +test('computeSkillFingerprint applies stored ignore files like the CLI', async () => { + const testCase = goldenVectors.cases.find((entry) => entry.name === 'stored-ignore-files'); + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsFile: { + findAll: async () => [ + ...(testCase.ignoreFiles || []).map((file) => ({ + file_path: file.path, + content: file.content, + is_binary: 0, + })), + ...testCase.files.map((file) => ({ + file_path: file.path, + content: file.content, + is_binary: file.isBinary ? 1 : 0, + })), + ], + }, + }); + service.ctx = createMockCtx(); + + const fingerprint = await service.computeSkillFingerprint(1); + assert.equal(fingerprint, skillFingerprint.fingerprintFromGoldenCase(testCase)); +}); + +test('resolveFingerprint ensures storage is ready before querying the skill model', async () => { + const events = []; + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => { + events.push('query'); + return null; + }, + }, + }); + service.ctx = createMockCtx(); + service.ctx.service = { + skills: { + ensureStorageReady: async () => { + events.push('migrate'); + }, + }, + }; + + await service.resolveFingerprint('missing-skill', 'hash'); + + assert.deepEqual(events, ['migrate', 'query']); +}); + +test('resolveFingerprint returns match and latestVersion for matching fingerprint', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ id: 1, slug: 'skill-a', version: '1.0.0' }), + }, + SkillsFile: { + findAll: async () => [{ file_path: 'SKILL.md', content: 'test', is_binary: 0 }], + }, + }); + service.ctx = createMockCtx(); + + const fp = await service.computeSkillFingerprint(1); + const result = await service.resolveFingerprint('skill-a', fp); + assert.ok(result); + assert.deepEqual(result.match, { version: '1.0.0' }); + assert.deepEqual(result.latestVersion, { version: '1.0.0' }); +}); + +test('resolveFingerprint returns null match for unmatched fingerprint but valid slug', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => ({ id: 1, slug: 'skill-a', version: '1.0.0' }), + }, + SkillsFile: { + findAll: async () => [{ file_path: 'SKILL.md', content: 'test', is_binary: 0 }], + }, + }); + service.ctx = createMockCtx(); + + const result = await service.resolveFingerprint('skill-a', 'wrong_hash'); + assert.ok(result); + assert.equal(result.match, null); + assert.deepEqual(result.latestVersion, { version: '1.0.0' }); +}); + +test('resolveFingerprint returns null values for missing slug', async () => { + const service = Object.create(ClawhubService.prototype); + service.app = createMockApp({ + SkillsItem: { + findOne: async () => null, + }, + }); + service.ctx = createMockCtx(); + + const result = await service.resolveFingerprint('nonexistent', 'any_hash'); + assert.ok(result); + assert.equal(result.match, null); + assert.equal(result.latestVersion, null); +}); + +// ============================================================ +// Phase 7: US5 Management Tests (T032-T033) +// ============================================================ + +test('deleteSkill sets is_delete to 1', async () => { + const service = Object.create(ClawhubService.prototype); + const skill = { update: async (data) => Object.assign(skill, data) }; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => skill, + }, + }); + service.ctx = createMockCtx(); + + const result = await service.deleteSkill('my-skill'); + assert.equal(result.ok, true); + assert.equal(skill.is_delete, 1); +}); + +test('undeleteSkill sets is_delete to 0', async () => { + const service = Object.create(ClawhubService.prototype); + const skill = { is_delete: 1, update: async (data) => Object.assign(skill, data) }; + service.app = createMockApp({ + SkillsItem: { + findOne: async () => skill, + }, + }); + service.ctx = createMockCtx(); + + const result = await service.undeleteSkill('my-skill'); + assert.equal(result.ok, true); + assert.equal(skill.is_delete, 0); +}); + +// ============================================================ +// Controller Tests +// ============================================================ + +test('ClawhubController returns flat JSON (no wrapper)', async () => { + const ClawhubController = require('../app/controller/clawhub'); + const controller = Object.create(ClawhubController.prototype); + controller.ctx = createMockCtx(); + controller.ctx.service = { + clawhub: { + getRegistryMetadata: async () => ({ apiBase: '/api/v1' }), + }, + }; + + await controller.registryMetadata(); + assert.deepEqual(controller.ctx.body, { apiBase: '/api/v1' }); +}); diff --git a/test/clawhub-integration.test.js b/test/clawhub-integration.test.js new file mode 100644 index 00000000..d6a8a491 --- /dev/null +++ b/test/clawhub-integration.test.js @@ -0,0 +1,177 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +/** + * Integration tests for clawhub API endpoints. + * + * These tests verify the controller + service integration with mocked + * Egg.js context. Full end-to-end tests requiring a running dev server + * and the clawhub CLI should be run manually per quickstart.md. + */ + +// Load controller and create mock ctx +const ClawhubController = require('../app/controller/clawhub'); + +function createMockCtx(query = {}, params = {}, body = {}, files = []) { + const ctx = { + origin: 'http://10.0.0.8:7001', + query, + params, + request: { body, files, headers: {} }, + throw(status, message) { + const err = new Error(message); + err.status = status; + throw err; + }, + logger: { warn: () => {}, info: () => {}, error: () => {} }, + set: () => {}, + status: 200, + body: null, + }; + return ctx; +} + +// Helper to build a controller with mocked service +function buildController(serviceMethods) { + const controller = Object.create(ClawhubController.prototype); + const skillLikeService = Object.create(require('../app/service/skillLike').prototype); + skillLikeService.resolveClientIp = () => '127.0.0.1'; + skillLikeService.like = async () => ({ liked: true, likeCount: 1 }); + skillLikeService.unlike = async () => ({ liked: false, likeCount: 0 }); + + controller.ctx = createMockCtx(); + controller.ctx.service = { + clawhub: serviceMethods, + skillLike: skillLikeService, + }; + return controller; +} + +test('registryMetadata endpoint returns flat JSON', async () => { + const controller = buildController({ + getRegistryMetadata: async (origin) => ({ + apiBase: origin, + authBase: null, + minCliVersion: '0.9.0', + }), + }); + + await controller.registryMetadata(); + assert.equal(controller.ctx.body.apiBase, 'http://10.0.0.8:7001'); + assert.equal(controller.ctx.body.authBase, null); +}); + +test('search endpoint passes query and limit to service', async () => { + const controller = buildController({ + searchSkills: async (q, limit) => { + assert.equal(q, 'react'); + assert.equal(limit, '10'); + return [{ slug: 'react-skill', displayName: 'React', version: '1.0.0', score: 1.0 }]; + }, + }); + controller.ctx.query = { q: 'react', limit: '10' }; + + await controller.search(); + assert.equal(controller.ctx.body.results.length, 1); + assert.equal(controller.ctx.body.results[0].slug, 'react-skill'); +}); + +test('detail endpoint returns 404 for missing skill', async () => { + const controller = buildController({ + getSkillDetail: async () => null, + }); + controller.ctx.params = { slug: 'missing' }; + + await controller.detail(); + assert.equal(controller.ctx.status, 404); + assert.equal(controller.ctx.body.error, '技能不存在'); +}); + +test('download endpoint sets correct headers', async () => { + const controller = buildController({ + buildSkillZip: async (slug) => { + assert.equal(slug, 'my-skill'); + return { fileName: 'my-skill-1.0.0.zip', content: Buffer.from('PK') }; + }, + }); + controller.ctx.query = { slug: 'my-skill', version: '1.0.0' }; + const headers = {}; + controller.ctx.set = (k, v) => { + headers[k] = v; + }; + + await controller.download(); + assert.equal(headers['Content-Type'], 'application/zip'); + assert.ok(headers['Content-Disposition'].includes('my-skill-1.0.0.zip')); + assert.ok(Buffer.isBuffer(controller.ctx.body)); +}); + +test('publish endpoint parses multipart payload JSON string', async () => { + const controller = buildController({ + publishSkill: async (payload, files) => { + assert.equal(payload.slug, 'test-skill'); + assert.equal(files.length, 1); + return { ok: true, skillId: 123, versionId: 'v1.0.0' }; + }, + }); + controller.ctx.request.body = { + payload: JSON.stringify({ slug: 'test-skill', displayName: 'Test', version: '1.0.0' }), + }; + controller.ctx.request.files = [{ filepath: 'SKILL.md', content: '# Test' }]; + + await controller.publish(); + assert.equal(controller.ctx.body.ok, true); +}); + +test('resolve endpoint returns correct match and latestVersion response', async () => { + const controller = buildController({ + resolveFingerprint: async (slug, hash) => { + assert.equal(slug, 'my-skill'); + assert.equal(hash, 'abc123'); + return { + match: { version: '1.0.0' }, + latestVersion: { version: '1.0.0' }, + }; + }, + }); + controller.ctx.query = { slug: 'my-skill', hash: 'abc123' }; + + await controller.resolve(); + assert.equal(controller.ctx.status, 200); + assert.deepEqual(controller.ctx.body, { + match: { version: '1.0.0' }, + latestVersion: { version: '1.0.0' }, + }); +}); + +test('star endpoint delegates to skillLike service', async () => { + const controller = buildController({}); + controller.ctx.params = { slug: 'my-skill' }; + controller.ctx.service.skillLike = { + resolveClientIp: () => '127.0.0.1', + like: async (slug, _ip) => { + assert.equal(slug, 'my-skill'); + return { liked: true, likeCount: 5 }; + }, + }; + + await controller.star(); + assert.equal(controller.ctx.body.starred, true); + assert.equal(controller.ctx.body.starCount, 5); +}); + +test('unstar endpoint delegates to skillLike service', async () => { + const controller = buildController({}); + controller.ctx.params = { slug: 'my-skill' }; + controller.ctx.service.skillLike = { + resolveClientIp: () => '127.0.0.1', + unlike: async (slug, _ip) => { + assert.equal(slug, 'my-skill'); + return { liked: false, likeCount: 4 }; + }, + }; + + await controller.unstar(); + assert.equal(controller.ctx.body.starred, false); + assert.equal(controller.ctx.body.starCount, 4); +}); diff --git a/test/github-stars.test.js b/test/github-stars.test.js new file mode 100644 index 00000000..0a570935 --- /dev/null +++ b/test/github-stars.test.js @@ -0,0 +1,81 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const GitHubStarsClient = require('../app/utils/github-stars'); + +test('extractGitHubRepoFullName parses SSH git@github.com URLs', () => { + const client = new GitHubStarsClient(); + assert.equal(client.extractGitHubRepoFullName('git@github.com:anthropics/claude-code.git'), 'anthropics/claude-code'); + assert.equal(client.extractGitHubRepoFullName('git@github.com:owner/repo'), 'owner/repo'); +}); + +test('extractGitHubRepoFullName parses HTTPS github.com URLs', () => { + const client = new GitHubStarsClient(); + assert.equal(client.extractGitHubRepoFullName('https://github.com/facebook/react'), 'facebook/react'); + assert.equal(client.extractGitHubRepoFullName('https://github.com/owner/repo.git'), 'owner/repo'); + assert.equal(client.extractGitHubRepoFullName('http://github.com/owner/repo'), 'owner/repo'); +}); + +test('extractGitHubRepoFullName returns empty for non-GitHub URLs', () => { + const client = new GitHubStarsClient(); + assert.equal(client.extractGitHubRepoFullName('https://gitlab.com/owner/repo'), ''); + assert.equal(client.extractGitHubRepoFullName(''), ''); + assert.equal(client.extractGitHubRepoFullName('not-a-url'), ''); +}); + +test('parseCompactNumber handles plain numbers', () => { + const client = new GitHubStarsClient(); + assert.equal(client.parseCompactNumber('42'), 42); + assert.equal(client.parseCompactNumber('1,234'), 1234); + assert.equal(client.parseCompactNumber('0'), 0); +}); + +test('parseCompactNumber handles compact suffixes', () => { + const client = new GitHubStarsClient(); + assert.equal(client.parseCompactNumber('1.5k'), 1500); + assert.equal(client.parseCompactNumber('2m'), 2000000); + assert.equal(client.parseCompactNumber('3b'), 3000000000); +}); + +test('parseCompactNumber returns null for invalid input', () => { + const client = new GitHubStarsClient(); + assert.equal(client.parseCompactNumber(''), null); + assert.equal(client.parseCompactNumber('abc'), null); + assert.equal(client.parseCompactNumber(null), null); +}); + +test('extractStarsFromGitHubHtml extracts stars from title attribute', () => { + const client = new GitHubStarsClient(); + const html = '1.2k'; + assert.equal(client.extractStarsFromGitHubHtml(html), 1234); +}); + +test('extractStarsFromGitHubHtml extracts stars from aria-label', () => { + const client = new GitHubStarsClient(); + const html = '5k'; + assert.equal(client.extractStarsFromGitHubHtml(html), 5000); +}); + +test('extractStarsFromGitHubHtml extracts stars from text content', () => { + const client = new GitHubStarsClient(); + const html = '42'; + assert.equal(client.extractStarsFromGitHubHtml(html), 42); +}); + +test('extractStarsFromGitHubHtml returns null for missing element', () => { + const client = new GitHubStarsClient(); + assert.equal(client.extractStarsFromGitHubHtml(''), null); + assert.equal(client.extractStarsFromGitHubHtml(''), null); +}); + +test('fetchByRepoUrl returns null for non-GitHub URLs', async () => { + const client = new GitHubStarsClient(); + const result = await client.fetchByRepoUrl('https://gitlab.com/owner/repo'); + assert.equal(result, null); +}); + +test('fetchByRepoUrl returns null for empty input', async () => { + const client = new GitHubStarsClient(); + const result = await client.fetchByRepoUrl(''); + assert.equal(result, null); +}); diff --git a/test/skill-fingerprint-contract.test.js b/test/skill-fingerprint-contract.test.js new file mode 100644 index 00000000..0907ac31 --- /dev/null +++ b/test/skill-fingerprint-contract.test.js @@ -0,0 +1,36 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const contract = require('../contracts/skill-fingerprint'); +const goldenVectors = require('../contracts/skill-fingerprint/golden-vectors.v1.json'); + +test('golden vectors match the shared fingerprint contract', () => { + for (const testCase of goldenVectors.cases) { + const fingerprint = contract.fingerprintFromGoldenCase(testCase); + if (testCase.fingerprint) { + assert.equal( + fingerprint, + testCase.fingerprint, + `golden vector "${testCase.name}" drifted` + ); + continue; + } + assert.equal( + contract.fingerprintFromGoldenCase(testCase), + fingerprint, + `golden vector "${testCase.name}" must be deterministic` + ); + } +}); + +test('buildSkillFingerprint sorts paths lexicographically', () => { + const fingerprint = contract.buildSkillFingerprint([ + { path: 'b.txt', sha256: contract.sha256Hex('b') }, + { path: 'a.txt', sha256: contract.sha256Hex('a') }, + ]); + const expected = contract.buildSkillFingerprint([ + { path: 'a.txt', sha256: contract.sha256Hex('a') }, + { path: 'b.txt', sha256: contract.sha256Hex('b') }, + ]); + assert.equal(fingerprint, expected); +}); diff --git a/test/skills-import-package-name.test.js b/test/skills-import-package-name.test.js new file mode 100644 index 00000000..c26f91f9 --- /dev/null +++ b/test/skills-import-package-name.test.js @@ -0,0 +1,134 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const skillsModule = require('../app/service/skills'); + +test('isLikelyBinary treats invalid UTF-8 as binary so original bytes are preserved', () => { + const service = Object.create(skillsModule.prototype); + + assert.equal(service.isLikelyBinary(Buffer.from('valid utf8', 'utf8')), false); + assert.equal(service.isLikelyBinary(Buffer.from([0xff, 0xfe, 0xfd])), true); + assert.equal(service.isLikelyBinary(Buffer.from([0x61, 0x00, 0x62])), true); +}); + +test('getUploadIdentityKey returns preferredName when provided', () => { + const result = skillsModule.prototype.getUploadIdentityKey( + [{ name: 'skill-a' }, { name: 'skill-b' }], + 'my-package' + ); + assert.equal(result, 'my-package'); +}); + +test('getUploadIdentityKey returns empty string when no preferredName', () => { + const result = skillsModule.prototype.getUploadIdentityKey( + [{ name: 'skill-b' }, { name: 'skill-a' }], + '' + ); + assert.equal(result, '', + 'Without preferredName, should return empty string to keep package name clean'); +}); + +test('buildUploadSourceMeta with packageName as identityKey produces readable sourceKey', () => { + const meta = skillsModule.prototype.buildUploadSourceMeta( + 'demo-multi-skill-folders.zip', + 'demo-multi-skill-folders' + ); + assert.ok(meta.sourceUrl.includes('demo-multi-skill-folders')); + assert.equal(meta.repoHost, 'upload'); + assert.equal(meta.sourceType, 'upload'); +}); + +test('buildUploadSourceMeta produces clean repoPath with or without packageName', () => { + const withPackage = skillsModule.prototype.buildUploadSourceMeta( + 'demo-multi-skill-folders.zip', + 'demo-multi-skill-folders' + ); + const withoutPackage = skillsModule.prototype.buildUploadSourceMeta( + 'skills-batch.zip', + '' + ); + assert.equal(withPackage.repoPath, 'demo-multi-skill-folders', + 'With packageName, repoPath should equal packageName'); + assert.equal(withoutPackage.repoPath, 'skills-batch', + 'Without packageName, repoPath should equal zip filename'); +}); + +test('persistSkillsForSource accepts preferredPackageName parameter', () => { + const fnStr = skillsModule.prototype.persistSkillsForSource.toString(); + assert.ok( + fnStr.includes('preferredPackageName'), + 'persistSkillsForSource should have preferredPackageName parameter' + ); +}); + +test('importSkillFile multi-skill upload uses clean zip filename as package name (BUG)', () => { + const service = Object.create(skillsModule.prototype); + service.sanitizeSlugSegment = skillsModule.prototype.sanitizeSlugSegment; + service.hashString = skillsModule.prototype.hashString; + + // Simulate web upload: no packageName, no skillName (multi-skill requires empty) + const fileName = 'test-skill-package.zip'; + const packageName = ''; + const skillName = ''; + const identityKey = packageName || skillName; // '' + + // First build: used for buildSkillSlug + const parsedSource = service.buildUploadSourceMeta(fileName, identityKey); + assert.equal(parsedSource.repoPath, 'test-skill-package', + 'First build should have clean repoPath from filename'); + + // Skill records from discoverSkillDirs + prepareSkillRecord + const skillRecords = [{ name: 'web-scraper' }, { name: 'api-tester' }]; + + // Second build: used for upsertSourceRecord and persistSkillsForSource + // This is where the bug manifests + const uploadSourceMeta = service.buildUploadSourceMeta( + fileName, + service.getUploadIdentityKey(skillRecords, identityKey) + ); + + // BUG: currently produces 'test-skill-package-api-tester-web-scraper-b0c3619b' + // EXPECTED: 'test-skill-package' (just the filename, readable) + assert.equal(uploadSourceMeta.repoPath, 'test-skill-package', + `Package name should be clean zip filename, got: ${uploadSourceMeta.repoPath}`); +}); + +test('importSkillFile single-skill upload with custom name uses name in repoPath', () => { + const service = Object.create(skillsModule.prototype); + service.sanitizeSlugSegment = skillsModule.prototype.sanitizeSlugSegment; + service.hashString = skillsModule.prototype.hashString; + + const fileName = 'my-skill.zip'; + const skillName = 'custom-skill-name'; + const identityKey = skillName; + + const skillRecords = [{ name: 'custom-skill-name' }]; + + const uploadSourceMeta = service.buildUploadSourceMeta( + fileName, + service.getUploadIdentityKey(skillRecords, identityKey) + ); + + assert.equal(uploadSourceMeta.repoPath, 'custom-skill-name', + 'Single-skill upload with custom name should use name as repoPath'); +}); + +test('importSkillFile CLI upload with packageName uses packageName as repoPath', () => { + const service = Object.create(skillsModule.prototype); + service.sanitizeSlugSegment = skillsModule.prototype.sanitizeSlugSegment; + service.hashString = skillsModule.prototype.hashString; + + const fileName = 'skills-batch.zip'; + const packageName = 'my-awesome-pack'; + const identityKey = packageName; + + const skillRecords = [{ name: 'skill-a' }, { name: 'skill-b' }]; + + const uploadSourceMeta = service.buildUploadSourceMeta( + fileName, + service.getUploadIdentityKey(skillRecords, identityKey) + ); + + assert.equal(uploadSourceMeta.repoPath, 'my-awesome-pack', + 'CLI upload with packageName should use packageName as repoPath'); +}); diff --git a/test/skills-install-key.test.js b/test/skills-install-key.test.js index 24e24d12..d520fefa 100644 --- a/test/skills-install-key.test.js +++ b/test/skills-install-key.test.js @@ -176,3 +176,775 @@ test('assertSkillNamesUnique rejects existing skill name', async () => { error.status === 400 && error.message === '技能名称“skill-creator”已存在,请更换名称' ); }); + +test('buildSkillSlug generates clean slug for upload source without prefix', () => { + const service = Object.create(SkillsService.prototype); + service.sanitizeSlugSegment = SkillsService.prototype.sanitizeSlugSegment; + service.hashString = SkillsService.prototype.hashString; + + const sourceMeta = { + repoHost: 'upload', + sourceType: 'upload', + repoPath: 'my-test-zip-1234', + }; + + // 网页端上传,ZIP内技能目录为 "my-skill" + const slug = service.buildSkillSlug(sourceMeta, 'my-skill', 'My Skill'); + assert.equal(slug, 'my-skill'); + + // 如果 relativeSkillPath 是 ".",应该回退使用 skillName + const slugDot = service.buildSkillSlug(sourceMeta, '.', 'My Skill'); + assert.equal(slugDot, 'my-skill'); +}); + +test('persistSkillsForSource - TDD scenarios for web upload', async () => { + const service = Object.create(SkillsService.prototype); + + service.fetchStarsBySourceRepo = async () => 0; + service.buildSkillSlug = SkillsService.prototype.buildSkillSlug; + service.sanitizeSlugSegment = SkillsService.prototype.sanitizeSlugSegment; + service.hashString = SkillsService.prototype.hashString; + + const dbSkills = []; + const dbFiles = []; + let idCounter = 1; + + const SkillsItem = { + findAll: async (options) => { + const sourceId = options.where.source_id; + return dbSkills.filter(item => item.source_id === sourceId && item.is_delete === 0); + }, + findOne: async (options) => { + const slug = options.where.slug; + return dbSkills.find(item => item.slug === slug) || null; + }, + create: async (payload) => { + const newItem = { + id: idCounter++, + ...payload, + update: async (fields) => { + Object.assign(newItem, fields); + return newItem; + }, + }; + dbSkills.push(newItem); + return newItem; + }, + }; + + const SkillsFile = { + destroy: async () => {}, + bulkCreate: async (rows) => { + dbFiles.push(...rows); + return rows; + }, + }; + + service.app = { + model: { + SkillsItem, + SkillsFile, + transaction: async (cb) => { + return await cb({}); + }, + }, + Sequelize: { + Op: { + in: 'in', + ne: 'ne', + }, + }, + }; + + service.ctx = { + throw(status, message) { + const error = new Error(message); + error.status = status; + throw error; + }, + }; + + const sourceMeta = { + repoHost: 'upload', + sourceType: 'upload', + repoPath: 'test-source-key', + }; + + // 1. 干净 Slug 首次导入(应该能创建成功) + const record1 = { + name: 'my-skill', + description: 'desc', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'my-skill', + skillMd: '# my-skill', + installCommand: '', + files: [], + }; + + const result1 = await service.persistSkillsForSource(10, sourceMeta, [record1]); + assert.equal(result1.length, 1); + assert.equal(result1[0].slug, 'my-skill'); + assert.equal(dbSkills.length, 1); + assert.equal(dbSkills[0].slug, 'my-skill'); + assert.equal(dbSkills[0].name, 'my-skill'); + assert.equal(dbSkills[0].is_delete, 0); + + // 2. 同名同 Slug 重复导入/同步(Upsert)—— 应该成功覆盖 + record1.description = 'new desc'; + const result2 = await service.persistSkillsForSource(10, sourceMeta, [record1]); + assert.equal(result2.length, 1); + assert.equal(dbSkills.length, 1); + assert.equal(dbSkills[0].description, 'new desc'); + + // 3. 不同名同 Slug 导入冲突(应抛出 400 错误:slug 已存在) + const recordConflict = { + name: 'Another Name', + description: 'desc', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'my-skill', + skillMd: '# another', + installCommand: '', + files: [], + }; + + await assert.rejects( + () => service.persistSkillsForSource(11, sourceMeta, [recordConflict]), + (error) => error.status === 400 && error.message === 'slug 已存在' + ); + + // 4. 对已软删除的同 slug 技能进行复用更新 + dbSkills[0].is_delete = 1; + + const recordReuse = { + name: 'reused-name', + description: 'reused-desc', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'my-skill', + skillMd: '# reused', + installCommand: '', + files: [], + }; + + const resultReuse = await service.persistSkillsForSource(12, sourceMeta, [recordReuse]); + assert.equal(resultReuse.length, 1); + assert.equal(dbSkills.length, 1); + assert.equal(dbSkills[0].name, 'reused-name'); + assert.equal(dbSkills[0].is_delete, 0); +}); + +test('assertSkillNamesUnique with excludeSlugs option ignores matching slug', async () => { + const service = Object.create(SkillsService.prototype); + service.ctx = { + throw(status, message) { + const error = new Error(message); + error.status = status; + throw error; + }, + }; + + service.app = { + model: { + SkillsItem: { + findOne: async (options) => { + const slugFilter = options.where.slug; + if (slugFilter && slugFilter['notIn'] && slugFilter['notIn'].includes('skill-a')) { + return null; + } + return { id: 9, name: 'skill-a', slug: 'skill-a' }; + }, + }, + }, + Sequelize: { + Op: { + in: 'in', + ne: 'ne', + notIn: 'notIn', + }, + }, + }; + + // 1. 如果不传 excludeSlugs,应该报错 + await assert.rejects( + () => service.assertSkillNamesUnique(['skill-a']), + (error) => error.status === 400 && error.message === '技能名称“skill-a”已存在,请更换名称' + ); + + // 2. 如果传入了匹配 of the excludeSlugs,应该不报错 + await service.assertSkillNamesUnique(['skill-a'], { + excludeSlugs: ['skill-a'], + }); +}); + +test('persistSkillsForSource - auto-package for multiple skill records and query filtering (T007)', async () => { + const service = Object.create(SkillsService.prototype); + service.fetchStarsBySourceRepo = async () => 0; + service.buildSkillSlug = SkillsService.prototype.buildSkillSlug; + service.sanitizeSlugSegment = SkillsService.prototype.sanitizeSlugSegment; + service.hashString = SkillsService.prototype.hashString; + + const dbSkills = []; + const dbFiles = []; + let idCounter = 1; + + const SkillsItem = { + findAll: async (options) => { + const sourceId = options.where.source_id; + // If filtering out sub-skills, parent_slug must be null + let result = dbSkills.filter(item => item.source_id === sourceId && item.is_delete === 0); + if (options.where && 'parent_slug' in options.where && options.where.parent_slug === null) { + result = result.filter(item => item.parent_slug === null); + } + return result; + }, + findOne: async (options) => { + const slug = options.where.slug; + return dbSkills.find(item => item.slug === slug) || null; + }, + create: async (payload) => { + const newItem = { + id: idCounter++, + ...payload, + update: async (fields) => { + Object.assign(newItem, fields); + return newItem; + }, + }; + dbSkills.push(newItem); + return newItem; + }, + }; + + const SkillsFile = { + destroy: async () => {}, + bulkCreate: async (rows) => { + dbFiles.push(...rows); + return rows; + }, + }; + + service.app = { + model: { + SkillsItem, + SkillsFile, + transaction: async (cb) => { + return await cb({}); + }, + }, + Sequelize: { + Op: { + in: 'in', + ne: 'ne', + }, + }, + }; + + service.ctx = { + throw(status, message) { + const error = new Error(message); + error.status = status; + throw error; + }, + }; + + const sourceMeta = { + repoHost: 'upload', + sourceType: 'upload', + repoPath: 'my-skills-pack', + }; + + // Two skill records + const records = [ + { + name: 'sub-skill-1', + description: 'desc1', + category: '通用', + version: '1.0.0', + tags: ['tag1'], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'skills/sub-1', + skillMd: '# sub 1', + installCommand: '', + files: [], + }, + { + name: 'sub-skill-2', + description: 'desc2', + category: '通用', + version: '1.0.0', + tags: ['tag2'], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'skills/sub-2', + skillMd: '# sub 2', + installCommand: '', + files: [], + }, + ]; + + await service.persistSkillsForSource(20, sourceMeta, records); + + // There should be a parent package item (is_package = 1) and two sub-skills (parent_slug = parentSlug) + const parent = dbSkills.find(item => item.is_package === 1); + assert.ok(parent, 'Should create a parent package'); + assert.equal(parent.name, 'my-skills-pack'); + assert.equal(parent.is_package, 1); + + const subSkills = dbSkills.filter(item => item.parent_slug === parent.slug); + assert.equal(subSkills.length, 2, 'Both sub-skills should refer to parent slug'); + assert.equal(subSkills[0].is_package, 0); + + // Check lists query filters out child skills + const mainList = await SkillsItem.findAll({ + where: { source_id: 20, is_delete: 0, parent_slug: null }, + }); + assert.equal(mainList.length, 1, 'Main list should only contain the parent package'); + assert.equal(mainList[0].slug, parent.slug); +}); + +test('getSkillArchive - skill package ZIP nested structures (T008)', async () => { + const service = Object.create(SkillsService.prototype); + service.app = { + model: { + SkillsItem: { + findAll: async (_options) => { + return [ + { id: 101, name: 'sub-skill-1', slug: 'sub-1', is_package: 0 }, + { id: 102, name: 'sub-skill-2', slug: 'sub-2', is_package: 0 }, + ]; + }, + }, + SkillsFile: { + findAll: async (options) => { + if (options.where.skill_id === 101) { + return [ + { file_path: 'README.md', content: 'c3ViLTE=', is_binary: 0 }, + ]; + } + if (options.where.skill_id === 102) { + return [ + { file_path: 'index.js', content: 'c3ViLTI=', is_binary: 0 }, + ]; + } + return []; + }, + }, + }, + }; + + service.ensureSkillCache = async () => {}; + service.getSkillByIdentifier = (_slug) => { + return { id: 100, name: 'my-skills-pack', slug: 'my-skills-pack', isPackage: 1 }; + }; + service.sanitizeFileName = (name) => name; + service.normalizeRelativePath = (p) => p; + service.decodeStoredFileContent = (content, _isBinary) => Buffer.from(content, 'base64'); + + const archive = await service.getSkillArchive('my-skills-pack'); + assert.ok(archive.content); + + const AdmZip = require('adm-zip'); + const zip = new AdmZip(archive.content); + const entries = zip.getEntries().map(e => e.entryName); + + assert.ok(entries.includes('my-skills-pack/sub-skill-1/README.md')); + assert.ok(entries.includes('my-skills-pack/sub-skill-2/index.js')); +}); + +// ============================================================ +// Phase 5: TDD Validation Tests (003-dt-skill-packages) +// ============================================================ + +test('ensureSkillsItemPackageColumns - auto-migration adds is_package and parent_slug when missing (T014)', async () => { + const service = Object.create(SkillsService.prototype); + + const addedColumns = []; + const queryInterface = { + describeTable: async () => ({ + id: { type: 'INTEGER' }, + slug: { type: 'VARCHAR' }, + name: { type: 'VARCHAR' }, + }), + addColumn: async (table, column, definition) => { + addedColumns.push({ table, column, definition }); + }, + }; + + service.app = { + model: { + getQueryInterface: () => queryInterface, + }, + Sequelize: { + TINYINT: 'TINYINT', + STRING(len) { this.len = len; }, + }, + }; + + await service.ensureSkillsItemPackageColumns(); + + assert.equal(addedColumns.length, 2, 'Should add both columns'); + assert.equal(addedColumns[0].column, 'is_package'); + assert.equal(addedColumns[1].column, 'parent_slug'); +}); + +test('ensureSkillsItemPackageColumns - skips migration when columns already exist (T014)', async () => { + const service = Object.create(SkillsService.prototype); + + let addColumnCalled = false; + const queryInterface = { + describeTable: async () => ({ + id: { type: 'INTEGER' }, + slug: { type: 'VARCHAR' }, + is_package: { type: 'TINYINT' }, + parent_slug: { type: 'VARCHAR' }, + }), + addColumn: async () => { + addColumnCalled = true; + }, + }; + + service.app = { + model: { + getQueryInterface: () => queryInterface, + }, + Sequelize: { + TINYINT: 'TINYINT', + STRING(len) { this.len = len; }, + }, + }; + + await service.ensureSkillsItemPackageColumns(); + + assert.equal(addColumnCalled, false, 'Should NOT add columns that already exist'); +}); + +test('persistSkillsForSource - single skill source does not create package (T015)', async () => { + const service = Object.create(SkillsService.prototype); + service.fetchStarsBySourceRepo = async () => 0; + service.buildSkillSlug = SkillsService.prototype.buildSkillSlug; + service.sanitizeSlugSegment = SkillsService.prototype.sanitizeSlugSegment; + service.hashString = SkillsService.prototype.hashString; + + const dbSkills = []; + let idCounter = 1; + + const SkillsItem = { + findAll: async () => dbSkills.filter(item => item.is_delete === 0), + findOne: async (opts) => dbSkills.find(item => item.slug === opts.where.slug) || null, + create: async (payload) => { + const newItem = { + id: idCounter++, + ...payload, + update: async (fields) => { Object.assign(newItem, fields); return newItem; }, + }; + dbSkills.push(newItem); + return newItem; + }, + }; + + const SkillsFile = { + destroy: async () => {}, + bulkCreate: async (rows) => rows, + }; + + service.app = { + model: { + SkillsItem, + SkillsFile, + transaction: async (cb) => cb({}), + }, + Sequelize: { Op: { in: 'in', ne: 'ne' } }, + }; + + service.ctx = { + throw(status, message) { const e = new Error(message); e.status = status; throw e; }, + }; + + const sourceMeta = { + repoHost: 'upload', + sourceType: 'upload', + repoPath: 'single-skill-zip', + }; + + const singleRecord = [{ + name: 'solo-skill', + description: 'a single skill', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'solo-skill', + skillMd: '# solo', + installCommand: '', + files: [], + }]; + + await service.persistSkillsForSource(30, sourceMeta, singleRecord); + + assert.equal(dbSkills.length, 1, 'Single source should create exactly 1 record'); + assert.equal(dbSkills[0].is_package, 0, 'Single skill should NOT be a package'); + assert.equal(dbSkills[0].parent_slug, null, 'Single skill should have no parent_slug'); + assert.equal(dbSkills[0].slug, 'solo-skill'); +}); + +test('persistSkillsForSource - multi-skill source creates parent package with children (T016)', async () => { + const service = Object.create(SkillsService.prototype); + service.fetchStarsBySourceRepo = async () => 5; + service.buildSkillSlug = SkillsService.prototype.buildSkillSlug; + service.sanitizeSlugSegment = SkillsService.prototype.sanitizeSlugSegment; + service.hashString = SkillsService.prototype.hashString; + + const dbSkills = []; + let idCounter = 1; + + const SkillsItem = { + findAll: async (opts) => { + const sourceId = opts.where.source_id; + return dbSkills.filter(item => item.source_id === sourceId && item.is_delete === 0); + }, + findOne: async (opts) => dbSkills.find(item => item.slug === opts.where.slug) || null, + create: async (payload) => { + const newItem = { + id: idCounter++, + ...payload, + update: async (fields) => { Object.assign(newItem, fields); return newItem; }, + }; + dbSkills.push(newItem); + return newItem; + }, + }; + + const SkillsFile = { + destroy: async () => {}, + bulkCreate: async (rows) => rows, + }; + + service.app = { + model: { + SkillsItem, + SkillsFile, + transaction: async (cb) => cb({}), + }, + Sequelize: { Op: { in: 'in', ne: 'ne' } }, + }; + + service.ctx = { + throw(status, message) { const e = new Error(message); e.status = status; throw e; }, + }; + + const sourceMeta = { + repoHost: 'upload', + sourceType: 'upload', + repoPath: 'mega-pack', + }; + + const multiRecords = [ + { + name: 'alpha-skill', + description: 'alpha', + category: '开发', + version: '1.0.0', + tags: ['a'], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'skills/alpha', + skillMd: '# alpha', + installCommand: '', + files: [], + }, + { + name: 'beta-skill', + description: 'beta', + category: '开发', + version: '1.0.0', + tags: ['b'], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'skills/beta', + skillMd: '# beta', + installCommand: '', + files: [], + }, + { + name: 'gamma-skill', + description: 'gamma', + category: '开发', + version: '1.0.0', + tags: ['g'], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'skills/gamma', + skillMd: '# gamma', + installCommand: '', + files: [], + }, + ]; + + await service.persistSkillsForSource(40, sourceMeta, multiRecords); + + const parents = dbSkills.filter(s => s.is_package === 1); + const children = dbSkills.filter(s => s.parent_slug !== null && s.parent_slug !== ''); + + assert.equal(parents.length, 1, 'Should create exactly 1 parent package'); + assert.equal(children.length, 3, 'Should create exactly 3 child skills'); + + const parent = parents[0]; + assert.equal(parent.is_package, 1); + assert.equal(parent.parent_slug, null, 'Parent should have null parent_slug'); + assert.equal(parent.name, 'mega-pack'); + assert.equal(parent.source_path, '.'); + assert.ok(parent.description.includes('alpha-skill'), 'Parent description should list children'); + + for (const child of children) { + assert.equal(child.parent_slug, parent.slug, `Child ${child.slug} should reference parent slug`); + assert.equal(child.is_package, 0, `Child ${child.slug} should NOT be a package`); + } +}); + +test('getSkillList API filters out child skills from main listing (T017)', () => { + // Directly test the filtering logic used by getSkillList + const cachedSkills = [ + { slug: 'indie-skill', name: 'Indie', isPackage: 0, parentSlug: null }, + { slug: 'my-pack', name: 'My Pack', isPackage: 1, parentSlug: null }, + { slug: 'child-a', name: 'Child A', isPackage: 0, parentSlug: 'my-pack' }, + { slug: 'child-b', name: 'Child B', isPackage: 0, parentSlug: 'my-pack' }, + ]; + + const list = cachedSkills.filter((item) => !item.parentSlug); + + assert.equal(list.length, 2, 'Main list should contain only indie skill + parent package'); + assert.equal(list[0].slug, 'indie-skill'); + assert.equal(list[1].slug, 'my-pack'); + + const childSlugs = list.map((s) => s.slug); + assert.ok(!childSlugs.includes('child-a'), 'child-a should be filtered out'); + assert.ok(!childSlugs.includes('child-b'), 'child-b should be filtered out'); +}); + +test('getSkillDetail returns children for package, none for regular skill (T018)', async () => { + const service = Object.create(SkillsService.prototype); + + const mockChildren = [ + { id: 201, slug: 'sub-1', name: 'Sub 1', is_package: 0, parent_slug: 'test-pack', is_delete: 0, stars: 0, updated_at_remote: new Date(), updated_at: new Date() }, + { id: 202, slug: 'sub-2', name: 'Sub 2', is_package: 0, parent_slug: 'test-pack', is_delete: 0, stars: 0, updated_at_remote: new Date(), updated_at: new Date() }, + ]; + + service.app = { + model: { + SkillsItem: { + findAll: async (opts) => { + if (opts.where.parent_slug === 'test-pack') { + return mockChildren; + } + return []; + }, + }, + SkillsFile: { + findAll: async () => [], + }, + }, + }; + + service.ensureSkillCache = async () => {}; + service.getSkillByIdentifier = (_slug) => { + if (_slug === 'test-pack') { + return { id: 100, slug: 'test-pack', name: 'Test Pack', isPackage: 1, parentSlug: null, skillMd: '# pack', stars: 10, category: '通用', version: '1.0.0', tags: [], allowedTools: [], sourceRepo: '', sourcePath: '.', description: 'test' }; + } + return { id: 200, slug: 'solo', name: 'Solo', isPackage: 0, parentSlug: null, skillMd: '# solo', stars: 5, category: '通用', version: '1.0.0', tags: [], allowedTools: [], sourceRepo: '', sourcePath: '.', description: 'test' }; + }; + service.toSkillDto = (row) => row; + service.toPublicSkill = (s) => ({ slug: s.slug, name: s.name, isPackage: s.isPackage || s.is_package, parentSlug: s.parentSlug || s.parent_slug, description: s.description || '' }); + + // Package detail should include children + const packDetail = await service.getSkillDetail('test-pack'); + assert.ok(Array.isArray(packDetail.children), 'Package detail should have children array'); + assert.equal(packDetail.children.length, 2, 'Package should have 2 children'); + assert.equal(packDetail.children[0].slug, 'sub-1'); + assert.equal(packDetail.children[1].slug, 'sub-2'); + + // Regular skill detail should NOT have children + const soloDetail = await service.getSkillDetail('solo'); + assert.equal(soloDetail.children, undefined, 'Regular skill should NOT have children'); +}); + +test('getSkillArchive - nested ZIP structure for package with multiple children (T019)', async () => { + const service = Object.create(SkillsService.prototype); + + const childSkills = [ + { id: 301, name: 'analyzer', slug: 'analyzer', is_package: 0 }, + { id: 302, name: 'planner', slug: 'planner', is_package: 0 }, + ]; + + const childFilesBySkillId = { + 301: [ + { file_path: 'SKILL.md', content: 'IyBBbmFseXplcg==', is_binary: 0 }, + { file_path: 'config.json', content: 'e30=', is_binary: 0 }, + ], + 302: [ + { file_path: 'SKILL.md', content: 'IyBQbGFubmVy', is_binary: 0 }, + { file_path: 'rules.md', content: 'IyBSdWxlcw==', is_binary: 0 }, + ], + }; + + service.app = { + model: { + SkillsItem: { + findAll: async (opts) => { + if (opts.where.parent_slug === 'ai-toolkit') { + return childSkills; + } + return []; + }, + }, + SkillsFile: { + findAll: async (opts) => childFilesBySkillId[opts.where.skill_id] || [], + }, + }, + }; + + service.ensureSkillCache = async () => {}; + service.getSkillByIdentifier = (_slug) => { + return { id: 300, name: 'AI Toolkit', slug: 'ai-toolkit', isPackage: 1 }; + }; + service.sanitizeFileName = (name) => name; + service.normalizeRelativePath = (p) => p; + service.decodeStoredFileContent = (content, _isBinary) => Buffer.from(content, 'base64'); + + const archive = await service.getSkillArchive('ai-toolkit'); + assert.ok(archive.content, 'Should return ZIP content'); + assert.equal(archive.fileName, 'AI Toolkit.zip', 'ZIP filename should match package name'); + + const AdmZip = require('adm-zip'); + const zip = new AdmZip(archive.content); + const entries = zip.getEntries().map((e) => e.entryName); + + // Verify nested structure: packageName/childName/filePath + assert.ok(entries.includes('AI Toolkit/analyzer/SKILL.md')); + assert.ok(entries.includes('AI Toolkit/analyzer/config.json')); + assert.ok(entries.includes('AI Toolkit/planner/SKILL.md')); + assert.ok(entries.includes('AI Toolkit/planner/rules.md')); + assert.equal(entries.length, 4, 'ZIP should contain exactly 4 files (2 per child)'); +}); + diff --git a/test/skills-package-stars.test.js b/test/skills-package-stars.test.js new file mode 100644 index 00000000..709d3e3c --- /dev/null +++ b/test/skills-package-stars.test.js @@ -0,0 +1,346 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const skillsModule = require('../app/service/skills'); +const SkillsService = skillsModule; + +// ============================================================ +// Phase 9: 技能包 Stars 聚合 (T038-T042) +// ============================================================ + +test('getSkillList aggregates package stars from children (T038)', async () => { + const service = Object.create(SkillsService.prototype); + service.toPublicSkill = SkillsService.prototype.toPublicSkill; + service.getSkillList = SkillsService.prototype.getSkillList; + + // Simulate skillCache with a package and its children + service.skillCache = { + loadedAt: Date.now(), + skills: [ + { + slug: 'my-pack', + installKey: 'my-pack', + name: 'My Pack', + description: 'A skill package', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 5, // This is the raw repo stars, should be overridden + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: '.', + installCommand: '', + isPackage: 1, + parentSlug: null, + skillMd: '# My Pack', + }, + { + slug: 'child-a', + installKey: 'child-a', + name: 'Child A', + description: 'First child', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 10, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: 'child-a', + installCommand: '', + isPackage: 0, + parentSlug: 'my-pack', + skillMd: '# Child A', + }, + { + slug: 'child-b', + installKey: 'child-b', + name: 'Child B', + description: 'Second child', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 25, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: 'child-b', + installCommand: '', + isPackage: 0, + parentSlug: 'my-pack', + skillMd: '# Child B', + }, + ], + categories: ['通用'], + bySlug: new Map(), + byInstallKey: new Map(), + }; + + // Build index maps + service.skillCache.skills.forEach((skill) => { + service.skillCache.bySlug.set(skill.slug, skill); + service.skillCache.byInstallKey.set(skill.installKey, skill); + }); + + const result = service.getSkillList({}); + + assert.equal(result.total, 1, 'List should only show parent package'); + assert.equal(result.list.length, 1); + assert.equal(result.list[0].slug, 'my-pack'); + assert.equal(result.list[0].stars, 35, + `Package stars should be sum of children (10 + 25 = 35), got: ${result.list[0].stars}`); +}); + +test('getSkillList children keep their original stars (T039)', async () => { + const service = Object.create(SkillsService.prototype); + service.toPublicSkill = SkillsService.prototype.toPublicSkill; + service.getSkillList = SkillsService.prototype.getSkillList; + + service.skillCache = { + loadedAt: Date.now(), + skills: [ + { + slug: 'my-pack', + installKey: 'my-pack', + name: 'My Pack', + description: 'A skill package', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 5, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: '.', + installCommand: '', + isPackage: 1, + parentSlug: null, + skillMd: '# My Pack', + }, + { + slug: 'child-a', + installKey: 'child-a', + name: 'Child A', + description: 'First child', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 42, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: 'child-a', + installCommand: '', + isPackage: 0, + parentSlug: 'my-pack', + skillMd: '# Child A', + }, + ], + categories: ['通用'], + bySlug: new Map(), + byInstallKey: new Map(), + }; + + service.skillCache.skills.forEach((skill) => { + service.skillCache.bySlug.set(skill.slug, skill); + service.skillCache.byInstallKey.set(skill.installKey, skill); + }); + + const result = service.getSkillList({}); + // Child should be filtered out from list + assert.equal(result.total, 1); + assert.equal(result.list[0].slug, 'my-pack'); + assert.equal(result.list[0].stars, 42, + 'Package stars should equal child stars (42)'); +}); + +test('getSkillDetail aggregates package stars from children (T039 variant)', async () => { + const service = Object.create(SkillsService.prototype); + service.toPublicSkill = SkillsService.prototype.toPublicSkill; + service.toSkillDto = SkillsService.prototype.toSkillDto; + service.parseJsonArray = SkillsService.prototype.parseJsonArray; + service.getSkillDetail = SkillsService.prototype.getSkillDetail; + + const mockChildren = [ + { + id: 201, + slug: 'sub-1', + name: 'Sub 1', + description: 'sub one', + category: '通用', + version: '1.0.0', + tags: [], + allowed_tools: '[]', + stars: 7, + updated_at_remote: new Date(), + updated_at: new Date(), + created_at: new Date(), + source_repo: '', + source_path: 'sub-1', + skill_md: '# Sub 1', + install_command: '', + file_count: 0, + is_package: 0, + parent_slug: 'test-pack', + is_delete: 0, + }, + { + id: 202, + slug: 'sub-2', + name: 'Sub 2', + description: 'sub two', + category: '通用', + version: '1.0.0', + tags: [], + allowed_tools: '[]', + stars: 13, + updated_at_remote: new Date(), + updated_at: new Date(), + created_at: new Date(), + source_repo: '', + source_path: 'sub-2', + skill_md: '# Sub 2', + install_command: '', + file_count: 0, + is_package: 0, + parent_slug: 'test-pack', + is_delete: 0, + }, + ]; + + service.app = { + model: { + SkillsFile: { + findAll: async () => [], + }, + SkillsItem: { + findAll: async (opts) => { + if (opts.where.parent_slug === 'test-pack') { + return mockChildren; + } + return []; + }, + }, + }, + }; + + service.ensureSkillCache = async () => {}; + service.getSkillByIdentifier = (_slug) => { + return { + id: 100, + slug: 'test-pack', + installKey: 'test-pack', + name: 'Test Pack', + description: 'test', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 3, // raw repo stars, should be overridden by children sum + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: '.', + installCommand: '', + isPackage: 1, + parentSlug: null, + skillMd: '# pack', + }; + }; + + const detail = await service.getSkillDetail('test-pack'); + + assert.equal(detail.stars, 20, + `Package detail stars should be sum of children (7 + 13 = 20), got: ${detail.stars}`); + assert.equal(detail.children.length, 2); + assert.equal(detail.children[0].stars, 7, 'Child should keep original stars'); + assert.equal(detail.children[1].stars, 13, 'Child should keep original stars'); +}); + +test('getSkillList does not duplicate count children in list (T040)', async () => { + const service = Object.create(SkillsService.prototype); + service.toPublicSkill = SkillsService.prototype.toPublicSkill; + service.getSkillList = SkillsService.prototype.getSkillList; + + service.skillCache = { + loadedAt: Date.now(), + skills: [ + { + slug: 'standalone-skill', + installKey: 'standalone-skill', + name: 'Standalone', + description: 'A standalone skill', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 100, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: 'standalone', + installCommand: '', + isPackage: 0, + parentSlug: null, + skillMd: '# Standalone', + }, + { + slug: 'my-pack', + installKey: 'my-pack', + name: 'My Pack', + description: 'A package', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 5, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: '.', + installCommand: '', + isPackage: 1, + parentSlug: null, + skillMd: '# My Pack', + }, + { + slug: 'child-1', + installKey: 'child-1', + name: 'Child 1', + description: 'Child skill', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + stars: 8, + updatedAt: new Date().toISOString(), + sourceRepo: '', + sourcePath: 'child-1', + installCommand: '', + isPackage: 0, + parentSlug: 'my-pack', + skillMd: '# Child 1', + }, + ], + categories: ['通用'], + bySlug: new Map(), + byInstallKey: new Map(), + }; + + service.skillCache.skills.forEach((skill) => { + service.skillCache.bySlug.set(skill.slug, skill); + service.skillCache.byInstallKey.set(skill.installKey, skill); + }); + + const result = service.getSkillList({}); + + assert.equal(result.total, 2, 'Should show standalone + package, not children'); + const slugs = result.list.map((s) => s.slug); + assert.ok(slugs.includes('standalone-skill')); + assert.ok(slugs.includes('my-pack')); + assert.ok(!slugs.includes('child-1'), 'Child skill should not appear in list'); + + const pack = result.list.find((s) => s.slug === 'my-pack'); + assert.equal(pack.stars, 8, 'Package stars should be sum of children (only one child = 8)'); + + const standalone = result.list.find((s) => s.slug === 'standalone-skill'); + assert.equal(standalone.stars, 100, 'Standalone skill stars should be unchanged'); +}); From 0403644f8c9af51e506557a26709ee59f99d7748 Mon Sep 17 00:00:00 2001 From: huaiju Date: Sun, 14 Jun 2026 21:22:52 +0800 Subject: [PATCH 02/13] feat: introduce skillsStorageReady middleware to ensure storage readiness for API routes --- app/middleware/skillsStorageReady.js | 6 +++ app/router.js | 32 +++++++----- app/service/clawhub.js | 14 ------ test/clawhub-contract.test.js | 52 -------------------- test/skills-storage-ready-middleware.test.js | 41 +++++++++++++++ 5 files changed, 66 insertions(+), 79 deletions(-) create mode 100644 app/middleware/skillsStorageReady.js create mode 100644 test/skills-storage-ready-middleware.test.js diff --git a/app/middleware/skillsStorageReady.js b/app/middleware/skillsStorageReady.js new file mode 100644 index 00000000..bc8c0fd4 --- /dev/null +++ b/app/middleware/skillsStorageReady.js @@ -0,0 +1,6 @@ +module.exports = () => { + return async function skillsStorageReady(ctx, next) { + await ctx.service.skills.ensureStorageReady(); + await next(); + }; +}; diff --git a/app/router.js b/app/router.js index 7877d5dd..9e3ca8d1 100644 --- a/app/router.js +++ b/app/router.js @@ -165,20 +165,26 @@ module.exports = (app) => { /** * Clawhub Registry API (v1) */ + const skillsStorageReady = app.middleware.skillsStorageReady(); + app.get('/.well-known/clawhub.json', app.controller.clawhub.registryMetadata); - app.get('/api/v1/search', app.controller.clawhub.search); - app.get('/api/v1/skills', app.controller.clawhub.list); - app.get('/api/v1/skills/:slug', app.controller.clawhub.detail); - app.get('/api/v1/skills/:slug/versions', app.controller.clawhub.versions); - app.get('/api/v1/skills/:slug/versions/:version', app.controller.clawhub.versionDetail); - app.get('/api/v1/skills/:slug/file', app.controller.clawhub.fileContent); - app.get('/api/v1/download', app.controller.clawhub.download); - app.get('/api/v1/resolve', app.controller.clawhub.resolve); - app.post('/api/v1/skills', app.controller.clawhub.publish); - app.delete('/api/v1/skills/:slug', app.controller.clawhub.delete); - app.post('/api/v1/skills/:slug/undelete', app.controller.clawhub.undelete); - app.post('/api/v1/stars/:slug', app.controller.clawhub.star); - app.delete('/api/v1/stars/:slug', app.controller.clawhub.unstar); + app.get('/api/v1/search', skillsStorageReady, app.controller.clawhub.search); + app.get('/api/v1/skills', skillsStorageReady, app.controller.clawhub.list); + app.get('/api/v1/skills/:slug', skillsStorageReady, app.controller.clawhub.detail); + app.get('/api/v1/skills/:slug/versions', skillsStorageReady, app.controller.clawhub.versions); + app.get( + '/api/v1/skills/:slug/versions/:version', + skillsStorageReady, + app.controller.clawhub.versionDetail + ); + app.get('/api/v1/skills/:slug/file', skillsStorageReady, app.controller.clawhub.fileContent); + app.get('/api/v1/download', skillsStorageReady, app.controller.clawhub.download); + app.get('/api/v1/resolve', skillsStorageReady, app.controller.clawhub.resolve); + app.post('/api/v1/skills', skillsStorageReady, app.controller.clawhub.publish); + app.delete('/api/v1/skills/:slug', skillsStorageReady, app.controller.clawhub.delete); + app.post('/api/v1/skills/:slug/undelete', skillsStorageReady, app.controller.clawhub.undelete); + app.post('/api/v1/stars/:slug', skillsStorageReady, app.controller.clawhub.star); + app.delete('/api/v1/stars/:slug', skillsStorageReady, app.controller.clawhub.unstar); // io.of('/').route('getShellCommand', io.controller.home.getShellCommand) // 暂时close Terminal相关功能 diff --git a/app/service/clawhub.js b/app/service/clawhub.js index 6ff47fa2..77f3007d 100644 --- a/app/service/clawhub.js +++ b/app/service/clawhub.js @@ -10,12 +10,6 @@ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\w.-]+)?(? const SKILL_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; class ClawhubService extends Service { - async ensureStorageReady() { - if (this.ctx.service?.skills?.ensureStorageReady) { - await this.ctx.service.skills.ensureStorageReady(); - } - } - // Well-Known Registry Metadata async getRegistryMetadata(origin) { return { @@ -27,7 +21,6 @@ class ClawhubService extends Service { // Search skills by name/description LIKE match async searchSkills(query, limit = 20) { - await this.ensureStorageReady(); const { SkillsItem } = this.app.model; const { Op } = this.app.Sequelize; const searchQuery = String(query || '').trim(); @@ -60,7 +53,6 @@ class ClawhubService extends Service { // List skills with cursor pagination and sorting async listSkills(cursor, sort, limit = 20) { - await this.ensureStorageReady(); const { SkillsItem } = this.app.model; const where = { is_delete: 0, parent_slug: null }; const sortMap = { @@ -133,7 +125,6 @@ class ClawhubService extends Service { // Get skill detail async _resolveSlug(slug) { - await this.ensureStorageReady(); const { SkillsItem } = this.app.model; let skill = await SkillsItem.findOne({ where: { slug, is_delete: 0 }, @@ -355,7 +346,6 @@ class ClawhubService extends Service { // Publish or update a skill async publishSkill(payload, files) { - await this.ensureStorageReady(); const { SkillsItem, SkillsFile, SkillsSource } = this.app.model; const { slug, displayName, version, tags } = payload; @@ -469,7 +459,6 @@ class ClawhubService extends Service { // Compute SHA256 fingerprint for a skill async computeSkillFingerprint(skillId) { - await this.ensureStorageReady(); const { SkillsFile } = this.app.model; const files = await SkillsFile.findAll({ where: { skill_id: skillId, is_delete: 0 }, @@ -484,7 +473,6 @@ class ClawhubService extends Service { // Resolve fingerprint to skill async resolveFingerprint(slug, hash) { - await this.ensureStorageReady(); const { SkillsItem } = this.app.model; const skill = await SkillsItem.findOne({ where: { slug, is_delete: 0 }, @@ -509,7 +497,6 @@ class ClawhubService extends Service { // Soft delete skill async deleteSkill(slug) { - await this.ensureStorageReady(); const { SkillsItem } = this.app.model; const skill = await SkillsItem.findOne({ where: { slug } }); if (!skill) { @@ -521,7 +508,6 @@ class ClawhubService extends Service { // Undelete skill async undeleteSkill(slug) { - await this.ensureStorageReady(); const { SkillsItem } = this.app.model; const skill = await SkillsItem.findOne({ where: { slug } }); if (!skill) { diff --git a/test/clawhub-contract.test.js b/test/clawhub-contract.test.js index 563553a2..0b0aa9ee 100644 --- a/test/clawhub-contract.test.js +++ b/test/clawhub-contract.test.js @@ -199,7 +199,6 @@ test('listSkills uses a composite cursor that matches newest sorting', async () }, }); service.ctx = createMockCtx(); - service.ctx.service = { skills: { ensureStorageReady: async () => {} } }; const firstPage = await service.listSkills(null, 'newest', 1); await service.listSkills(firstPage.nextCursor, 'newest', 1); @@ -249,7 +248,6 @@ test('listSkills uses a composite cursor that matches stars sorting', async () = }, }); service.ctx = createMockCtx(); - service.ctx.service = { skills: { ensureStorageReady: async () => {} } }; const firstPage = await service.listSkills(null, 'stars', 1); await service.listSkills(firstPage.nextCursor, 'stars', 1); @@ -267,31 +265,6 @@ test('listSkills uses a composite cursor that matches stars sorting', async () = ]); }); -test('searchSkills ensures the skills schema is ready before querying', async () => { - const events = []; - const service = Object.create(ClawhubService.prototype); - service.app = createMockApp({ - SkillsItem: { - findAll: async () => { - events.push('query'); - return []; - }, - }, - }); - service.ctx = createMockCtx(); - service.ctx.service = { - skills: { - ensureStorageReady: async () => { - events.push('migrate'); - }, - }, - }; - - await service.searchSkills('demo', 10); - - assert.deepEqual(events, ['migrate', 'query']); -}); - test('getSkillDetail returns full skill object', async () => { const service = Object.create(ClawhubService.prototype); service.app = createMockApp({ @@ -744,31 +717,6 @@ test('computeSkillFingerprint applies stored ignore files like the CLI', async ( assert.equal(fingerprint, skillFingerprint.fingerprintFromGoldenCase(testCase)); }); -test('resolveFingerprint ensures storage is ready before querying the skill model', async () => { - const events = []; - const service = Object.create(ClawhubService.prototype); - service.app = createMockApp({ - SkillsItem: { - findOne: async () => { - events.push('query'); - return null; - }, - }, - }); - service.ctx = createMockCtx(); - service.ctx.service = { - skills: { - ensureStorageReady: async () => { - events.push('migrate'); - }, - }, - }; - - await service.resolveFingerprint('missing-skill', 'hash'); - - assert.deepEqual(events, ['migrate', 'query']); -}); - test('resolveFingerprint returns match and latestVersion for matching fingerprint', async () => { const service = Object.create(ClawhubService.prototype); service.app = createMockApp({ diff --git a/test/skills-storage-ready-middleware.test.js b/test/skills-storage-ready-middleware.test.js new file mode 100644 index 00000000..ca704969 --- /dev/null +++ b/test/skills-storage-ready-middleware.test.js @@ -0,0 +1,41 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const skillsStorageReady = require('../app/middleware/skillsStorageReady')(); + +test('skillsStorageReady middleware runs migration before the route handler', async () => { + const events = []; + const ctx = { + service: { + skills: { + ensureStorageReady: async () => { + events.push('migrate'); + }, + }, + }, + }; + + await skillsStorageReady(ctx, async () => { + events.push('handler'); + }); + + assert.deepEqual(events, ['migrate', 'handler']); +}); + +test('skillsStorageReady middleware propagates handler errors after migration', async () => { + const ctx = { + service: { + skills: { + ensureStorageReady: async () => {}, + }, + }, + }; + + await assert.rejects( + () => + skillsStorageReady(ctx, async () => { + throw new Error('handler failed'); + }), + /handler failed/ + ); +}); From f1e1ff635d885d831395282bd1634a11d410edbd Mon Sep 17 00:00:00 2001 From: huaiju Date: Sun, 14 Jun 2026 21:28:51 +0800 Subject: [PATCH 03/13] feat: refactor SkillDetailContent and introduce new components for improved structure and functionality --- .../skills/detail/SkillDetailContent.tsx | 852 ++---------------- .../skills/detail/components/DetailIcon.tsx | 16 + .../detail/components/SkillDetailHero.tsx | 123 +++ .../detail/components/SkillDocViewer.tsx | 112 +++ .../detail/components/SkillFileExplorer.tsx | 137 +++ .../detail/components/SkillInstallPanel.tsx | 305 +++++++ app/web/pages/skills/detail/style.scss | 2 +- .../skills/detail/utils/skillDetailUtils.ts | 156 ++++ 8 files changed, 916 insertions(+), 787 deletions(-) create mode 100644 app/web/pages/skills/detail/components/DetailIcon.tsx create mode 100644 app/web/pages/skills/detail/components/SkillDetailHero.tsx create mode 100644 app/web/pages/skills/detail/components/SkillDocViewer.tsx create mode 100644 app/web/pages/skills/detail/components/SkillFileExplorer.tsx create mode 100644 app/web/pages/skills/detail/components/SkillInstallPanel.tsx create mode 100644 app/web/pages/skills/detail/utils/skillDetailUtils.ts diff --git a/app/web/pages/skills/detail/SkillDetailContent.tsx b/app/web/pages/skills/detail/SkillDetailContent.tsx index 0a0bf9d2..492c6353 100644 --- a/app/web/pages/skills/detail/SkillDetailContent.tsx +++ b/app/web/pages/skills/detail/SkillDetailContent.tsx @@ -1,219 +1,26 @@ import React, { useEffect, useMemo, useState } from 'react'; -import SyntaxHighlighter from 'react-syntax-highlighter'; -import { atomOneLight } from 'react-syntax-highlighter/dist/cjs/styles/hljs'; -import { - ArrowLeftOutlined, - LikeOutlined, - QuestionCircleOutlined, - StarOutlined, -} from '@ant-design/icons'; -import { Breadcrumb, Button, Col, Empty, Row, Spin, Tree, Typography } from 'antd'; -import type { DataNode } from 'antd/lib/tree'; +import { Button, Empty, Spin } from 'antd'; import { API } from '@/api'; -import agentIcon from '@/asset/images/skills-detail-figma/agent.svg'; -import chevronDownIcon from '@/asset/images/skills-detail-figma/chevron-down.svg'; -import chevronRightIcon from '@/asset/images/skills-detail-figma/chevron-right.svg'; -import contributorOne from '@/asset/images/skills-detail-figma/contributor-1.png'; -import contributorTwo from '@/asset/images/skills-detail-figma/contributor-2.png'; -import copyDarkIcon from '@/asset/images/skills-detail-figma/copy-dark.svg'; -import downloadIcon from '@/asset/images/skills-detail-figma/download.svg'; -import emptyRelatedIcon from '@/asset/images/skills-detail-figma/empty-related.svg'; -import externalLinkXsIcon from '@/asset/images/skills-detail-figma/external-link-xs.svg'; -import fileDocIcon from '@/asset/images/skills-detail-figma/file-doc.svg'; -import folderOpenBlueIcon from '@/asset/images/skills-detail-figma/folder-open-blue.svg'; -import heroSkillIcon from '@/asset/images/skills-detail-figma/hero-skill.svg'; -import humanIcon from '@/asset/images/skills-detail-figma/human.svg'; -import relatedSkillDocsIcon from '@/asset/images/skills-detail-figma/related-skill-docs.svg'; -import relatedSkillSecurityIcon from '@/asset/images/skills-detail-figma/related-skill-security.svg'; -import relatedSkillSqlIcon from '@/asset/images/skills-detail-figma/related-skill-sql.svg'; -import MarkdownRenderer from '@/components/markdownRenderer'; -import { SkillCard } from '@/components/skills/SkillCard'; -import { copyToClipboard } from '@/utils/copyUtils'; import { SkillDetail, SkillFileContent, SkillInstallMeta, SkillItem } from '../types'; +import { SkillDetailHero } from './components/SkillDetailHero'; +import { SkillDocViewer } from './components/SkillDocViewer'; +import { SkillFileExplorer } from './components/SkillFileExplorer'; +import { InstallPanelKey, SkillInstallPanel } from './components/SkillInstallPanel'; +import { + buildFileTreeData, + formatDownloadCommand, + normalizeSourceUrl, + SkillDetailHistory, +} from './utils/skillDetailUtils'; import './style.scss'; -const { Title, Paragraph } = Typography; - -interface SkillTreeNode extends DataNode { - children?: SkillTreeNode[]; -} - -interface FrontmatterItem { - key: string; - value: string; -} - interface SkillDetailContentProps { slug: string; parentSlug?: string; - history: any; -} - -interface FigmaIconProps { - src: string; - className?: string; - alt?: string; + history: SkillDetailHistory; } -type InstallPanelKey = 'agent' | 'human' | null; - -const relatedSkillIconUrls = [relatedSkillSqlIcon, relatedSkillSecurityIcon, relatedSkillDocsIcon]; - -const relatedSkillShellClasses = ['is-blue', 'is-green', 'is-orange']; -const browseMarketArrowIcon = externalLinkXsIcon; - -const FigmaIcon: React.FC = ({ src, className = '', alt = '' }) => ( - -); - -const formatFileSize = (size = 0) => { - if (size < 1024) return `${size} B`; - if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; - return `${(size / 1024 / 1024).toFixed(1)} MB`; -}; - -const sortTreeNodes = (nodes: SkillTreeNode[]) => { - nodes.sort((a, b) => { - const aIsLeaf = Boolean(a.isLeaf); - const bIsLeaf = Boolean(b.isLeaf); - if (aIsLeaf !== bIsLeaf) return aIsLeaf ? 1 : -1; - return String(a.title).localeCompare(String(b.title)); - }); - - nodes.forEach((node) => { - if (node.children && node.children.length > 0) { - sortTreeNodes(node.children); - } - }); -}; - -const buildFileTreeData = (fileList: string[]): SkillTreeNode[] => { - const treeData: SkillTreeNode[] = []; - - fileList.forEach((filePath) => { - const segments = filePath.split('/').filter(Boolean); - let currentNodes = treeData; - let currentPath = ''; - - segments.forEach((segment, index) => { - currentPath = currentPath ? `${currentPath}/${segment}` : segment; - const isLeaf = index === segments.length - 1; - let node = currentNodes.find((item) => item.key === currentPath); - - if (!node) { - node = { - key: currentPath, - title: segment, - isLeaf, - children: isLeaf ? undefined : [], - }; - currentNodes.push(node); - } - - if (!isLeaf) { - node.children = node.children || []; - currentNodes = node.children; - } - }); - }); - - sortTreeNodes(treeData); - return treeData; -}; - -const normalizeSourceUrl = (sourceRepo: string) => { - if (!sourceRepo) return ''; - const normalized = sourceRepo.replace(/^git\+/, '').trim(); - const sshMatch = normalized.match(/^git@([^:]+):(.+?)(?:\.git)?$/); - if (sshMatch) { - return `https://${sshMatch[1]}/${sshMatch[2]}`; - } - if (/^https?:\/\//.test(normalized)) { - return normalized.replace(/\.git$/, ''); - } - return ''; -}; - -const normalizeFrontmatterValue = (value: string) => { - const trimmed = String(value || '').trim(); - if (!trimmed) return '-'; - if ( - (trimmed.startsWith('"') && trimmed.endsWith('"')) || - (trimmed.startsWith("'") && trimmed.endsWith("'")) - ) { - return trimmed.slice(1, -1); - } - return trimmed; -}; - -const parseMarkdownFrontmatter = ( - markdown = '' -): { frontmatter: FrontmatterItem[]; body: string } => { - const content = String(markdown || ''); - const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); - if (!match) { - return { - frontmatter: [], - body: content, - }; - } - - const block = match[1] || ''; - const lines = block.split(/\r?\n/); - const frontmatter: FrontmatterItem[] = []; - let currentKey = ''; - let currentValueLines: string[] = []; - - const pushCurrent = () => { - if (!currentKey) return; - frontmatter.push({ - key: currentKey, - value: normalizeFrontmatterValue(currentValueLines.join('\n')), - }); - }; - - lines.forEach((line) => { - const keyMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/); - const isTopLevelKey = Boolean(keyMatch) && !/^\s/.test(line); - - if (isTopLevelKey && keyMatch) { - pushCurrent(); - currentKey = keyMatch[1]; - currentValueLines = [keyMatch[2] || '']; - return; - } - - if (currentKey) { - currentValueLines.push(line); - } - }); - - pushCurrent(); - - return { - frontmatter, - body: content.slice(match[0].length), - }; -}; - -const formatDownloadCommand = (downloadUrl = '', fileName = 'skill.zip') => { - if (!downloadUrl) return ''; - return `curl -L "${downloadUrl}" -o ${fileName}`; -}; - -const formatCompactDate = (value?: string) => { - if (!value) return '-'; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return '-'; - return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; -}; - const SkillDetailContent: React.FC = ({ slug, parentSlug: routeParentSlug, @@ -252,9 +59,6 @@ const SkillDetailContent: React.FC = ({ () => `npx dt-skill install ${installKey} --registry ${currentOrigin}`, [installKey, currentOrigin] ); - const browseMarketPath = '/page/skills'; - const cliInstallPlaceholderCommand = - '# 待补齐 Doraemon CLI 安装脚本 URL,例如 curl -fsSL | bash'; const archiveFileName = useMemo(() => { const rawName = detail?.name || slug || 'skill'; const normalized = rawName @@ -272,19 +76,6 @@ const SkillDetailContent: React.FC = ({ } return formatDownloadCommand(`${currentOrigin}${downloadPath}`, archiveFileName); }, [archiveFileName, currentOrigin, downloadPath, installMeta?.downloadUrl]); - const heroMetaItems = useMemo( - () => [ - { - label: '分类', - value: detail?.category || '通用', - }, - { - label: '最近更新', - value: formatCompactDate(detail?.updatedAt), - }, - ], - [detail?.category, detail?.updatedAt] - ); const isInstallable = Boolean(installMeta?.installable); const manualDownloadUrl = installMeta?.downloadUrl || downloadPath; const agentTerminalCommand = isInstallable ? skillInstallCommand : downloadCommand; @@ -294,6 +85,7 @@ const SkillDetailContent: React.FC = ({ const sentence = rawText.split(/(?<=[.!?。!?])/)[0]?.trim() || rawText; return sentence; }, [detail?.description]); + const handleSelectFile = (nextPath: string) => { if (!nextPath || nextPath === uiSelectedFilePath) return; setUiSelectedFilePath(nextPath); @@ -447,115 +239,6 @@ const SkillDetailContent: React.FC = ({ }; }, [selectedFilePath, slug]); - const renderFileViewer = () => { - if (fileLoading) { - return ( -
- -
- ); - } - - if (!fileContent) { - return ; - } - - if (fileContent.isBinary) { - return ; - } - - if (fileContent.language === 'markdown') { - const parsedMarkdown = parseMarkdownFrontmatter(fileContent.content || ''); - return ( -
- {parsedMarkdown.frontmatter.length > 0 ? ( -
- - - {parsedMarkdown.frontmatter.map((item) => { - const isCodeStyle = - item.value.includes('\n') || - item.value.startsWith('{') || - item.value.startsWith('['); - return ( - - - - - ); - })} - -
{item.key} - {isCodeStyle ? ( -
-                                                            {item.value}
-                                                        
- ) : ( - {item.value} - )} -
-
- ) : null} - {parsedMarkdown.body.trim() ? ( - - ) : null} -
- ); - } - - return ( - - {fileContent.content || ''} - - ); - }; - - const renderInlineCommand = (command: string, copyMessage: string, compact = false) => ( -
-
-
- {command || '暂无可复制命令'} -
-
-
- ); - - const renderTerminalCommand = (command: string, copyMessage: string) => ( -
-
-
- - - -
- BASH -
-
- $ - {command || '暂无可复制命令'} -
-
- ); - if (loading) { return (
@@ -574,466 +257,63 @@ const SkillDetailContent: React.FC = ({ ); } + const isPackage = detail.isPackage === 1; + return (
-
- {detail.isPackage !== 1 && ( - - )} - -
- - {detail.parentSlug ? ( - history.push(`/page/skills/${detail.parentSlug}`)} - > - {detail.parentSlug} - - ) : ( - history.push('/page/skills')} - > - 技能列表 - - )} - {detail.name} - - -
-
-
- -
- {detail.name} - - {heroSummary} - -
-
- -
- -
-
- -
- {heroMetaItems.map((item) => ( -
- {item.label} - {item.value} -
- ))} -
-
- - {detail.isPackage === 1 && detail.children && detail.children.length > 0 ? ( -
-
- 📦 - - 包含 {detail.children.length} 个子技能 - -
- - {detail.children.map((child) => ( - - - history.push(`/page/skills/${slug}/${s.slug}`) - } - showMeta={false} - size="compact" - /> - - ))} - -
- ) : ( -
-
-
- - {uiSelectedFilePath || 'SKILL.md'} -
-
- {fileLoading ? ( - - - - ) : null} - - - -
-
-
-
{renderFileViewer()}
-
-
- )} +
+ {!isPackage ? ( + setActiveInstallPanel('agent')} + /> + ) : null} + +
+ + + {!isPackage ? ( + + ) : null}
- {detail.isPackage !== 1 && ( - - )} + {!isPackage ? ( + + ) : null}
); diff --git a/app/web/pages/skills/detail/components/DetailIcon.tsx b/app/web/pages/skills/detail/components/DetailIcon.tsx new file mode 100644 index 00000000..58210cf7 --- /dev/null +++ b/app/web/pages/skills/detail/components/DetailIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface DetailIconProps { + src: string; + className?: string; + alt?: string; +} + +export const DetailIcon: React.FC = ({ src, className = '', alt = '' }) => ( + +); diff --git a/app/web/pages/skills/detail/components/SkillDetailHero.tsx b/app/web/pages/skills/detail/components/SkillDetailHero.tsx new file mode 100644 index 00000000..b6c8f4fb --- /dev/null +++ b/app/web/pages/skills/detail/components/SkillDetailHero.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { LikeOutlined, StarOutlined } from '@ant-design/icons'; +import { Breadcrumb, Button, Col, Row, Typography } from 'antd'; + +import heroSkillIcon from '@/asset/images/skills-detail-figma/hero-skill.svg'; +import { SkillCard } from '@/components/skills/SkillCard'; +import { SkillDetail, SkillItem } from '../../types'; +import type { SkillDetailHistory } from '../utils/skillDetailUtils'; +import { formatCompactDate } from '../utils/skillDetailUtils'; +import { DetailIcon } from './DetailIcon'; + +const { Title, Paragraph } = Typography; + +interface SkillDetailHeroProps { + detail: SkillDetail; + slug: string; + history: SkillDetailHistory; + heroSummary: string; + likeStatus: { liked: boolean; likeCount: number }; + likeLoading: boolean; + onLike: () => void; +} + +export const SkillDetailHero: React.FC = ({ + detail, + slug, + history, + heroSummary, + likeStatus, + likeLoading, + onLike, +}) => { + const heroMetaItems = [ + { + label: '分类', + value: detail.category || '通用', + }, + { + label: '最近更新', + value: formatCompactDate(detail.updatedAt), + }, + ]; + + return ( + <> + + {detail.parentSlug ? ( + history.push(`/page/skills/${detail.parentSlug}`)} + > + {detail.parentSlug} + + ) : ( + history.push('/page/skills')} + > + 技能列表 + + )} + {detail.name} + + +
+
+
+ +
+ {detail.name} + {heroSummary} +
+
+ +
+ +
+
+ +
+ {heroMetaItems.map((item) => ( +
+ {item.label} + {item.value} +
+ ))} +
+
+ + {detail.isPackage === 1 && detail.children && detail.children.length > 0 ? ( +
+
+ 📦 + 包含 {detail.children.length} 个子技能 +
+ + {detail.children.map((child: SkillItem) => ( + + history.push(`/page/skills/${slug}/${s.slug}`)} + showMeta={false} + size="compact" + /> + + ))} + +
+ ) : null} + + ); +}; diff --git a/app/web/pages/skills/detail/components/SkillDocViewer.tsx b/app/web/pages/skills/detail/components/SkillDocViewer.tsx new file mode 100644 index 00000000..4a000abc --- /dev/null +++ b/app/web/pages/skills/detail/components/SkillDocViewer.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import SyntaxHighlighter from 'react-syntax-highlighter'; +import { atomOneLight } from 'react-syntax-highlighter/dist/cjs/styles/hljs'; +import { Empty, Spin } from 'antd'; + +import fileDocIcon from '@/asset/images/skills-detail-figma/file-doc.svg'; +import MarkdownRenderer from '@/components/markdownRenderer'; +import { SkillFileContent } from '../../types'; +import { parseMarkdownFrontmatter } from '../utils/skillDetailUtils'; +import { DetailIcon } from './DetailIcon'; + +interface SkillDocViewerProps { + selectedFilePath: string; + fileLoading: boolean; + fileContent: SkillFileContent | null; +} + +const renderFileBody = (fileContent: SkillFileContent | null) => { + if (!fileContent) { + return ; + } + + if (fileContent.isBinary) { + return ; + } + + if (fileContent.language === 'markdown') { + const parsedMarkdown = parseMarkdownFrontmatter(fileContent.content || ''); + return ( +
+ {parsedMarkdown.frontmatter.length > 0 ? ( +
+ + + {parsedMarkdown.frontmatter.map((item) => { + const isCodeStyle = + item.value.includes('\n') || + item.value.startsWith('{') || + item.value.startsWith('['); + return ( + + + + + ); + })} + +
{item.key} + {isCodeStyle ? ( +
+                                                        {item.value}
+                                                    
+ ) : ( + {item.value} + )} +
+
+ ) : null} + {parsedMarkdown.body.trim() ? ( + + ) : null} +
+ ); + } + + return ( + + {fileContent.content || ''} + + ); +}; + +export const SkillDocViewer: React.FC = ({ + selectedFilePath, + fileLoading, + fileContent, +}) => ( +
+
+
+ + {selectedFilePath || 'SKILL.md'} +
+
+ {fileLoading ? ( + + + + ) : null} + + + +
+
+
+
+ {fileLoading ? ( +
+ +
+ ) : ( + renderFileBody(fileContent) + )} +
+
+
+); diff --git a/app/web/pages/skills/detail/components/SkillFileExplorer.tsx b/app/web/pages/skills/detail/components/SkillFileExplorer.tsx new file mode 100644 index 00000000..57642946 --- /dev/null +++ b/app/web/pages/skills/detail/components/SkillFileExplorer.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { ArrowLeftOutlined, QuestionCircleOutlined } from '@ant-design/icons'; +import { Button, Empty, Tree } from 'antd'; + +import fileDocIcon from '@/asset/images/skills-detail-figma/file-doc.svg'; +import folderOpenBlueIcon from '@/asset/images/skills-detail-figma/folder-open-blue.svg'; +import { copyToClipboard } from '@/utils/copyUtils'; + +import type { SkillDetailHistory, SkillTreeNode } from '../utils/skillDetailUtils'; +import { DetailIcon } from './DetailIcon'; + +interface SkillFileExplorerProps { + version?: string; + parentSlug?: string | null; + routeParentSlug?: string; + history: SkillDetailHistory; + fileTreeData: SkillTreeNode[]; + selectedFilePath: string; + onSelectFile: (path: string) => void; + isInstallable: boolean; + skillInstallCommand: string; + downloadCommand: string; + sourceUrl: string; + onFocusInstallPanel: () => void; +} + +export const SkillFileExplorer: React.FC = ({ + version, + parentSlug, + routeParentSlug, + history, + fileTreeData, + selectedFilePath, + onSelectFile, + isInstallable, + skillInstallCommand, + downloadCommand, + sourceUrl, + onFocusInstallPanel, +}) => { + const handleBack = () => { + const parent = parentSlug || routeParentSlug; + if (parent) { + history.push(`/page/skills/${parent}`); + return; + } + history.push('/page/skills'); + }; + + return ( + + ); +}; diff --git a/app/web/pages/skills/detail/components/SkillInstallPanel.tsx b/app/web/pages/skills/detail/components/SkillInstallPanel.tsx new file mode 100644 index 00000000..a2a099b9 --- /dev/null +++ b/app/web/pages/skills/detail/components/SkillInstallPanel.tsx @@ -0,0 +1,305 @@ +import React from 'react'; +import { Button } from 'antd'; + +import agentIcon from '@/asset/images/skills-detail-figma/agent.svg'; +import chevronDownIcon from '@/asset/images/skills-detail-figma/chevron-down.svg'; +import chevronRightIcon from '@/asset/images/skills-detail-figma/chevron-right.svg'; +import contributorOne from '@/asset/images/skills-detail-figma/contributor-1.png'; +import contributorTwo from '@/asset/images/skills-detail-figma/contributor-2.png'; +import copyDarkIcon from '@/asset/images/skills-detail-figma/copy-dark.svg'; +import downloadIcon from '@/asset/images/skills-detail-figma/download.svg'; +import emptyRelatedIcon from '@/asset/images/skills-detail-figma/empty-related.svg'; +import externalLinkXsIcon from '@/asset/images/skills-detail-figma/external-link-xs.svg'; +import humanIcon from '@/asset/images/skills-detail-figma/human.svg'; +import relatedSkillDocsIcon from '@/asset/images/skills-detail-figma/related-skill-docs.svg'; +import relatedSkillSecurityIcon from '@/asset/images/skills-detail-figma/related-skill-security.svg'; +import relatedSkillSqlIcon from '@/asset/images/skills-detail-figma/related-skill-sql.svg'; +import { copyToClipboard } from '@/utils/copyUtils'; +import { SkillFileContent, SkillItem } from '../../types'; +import type { SkillDetailHistory } from '../utils/skillDetailUtils'; +import { formatFileSize } from '../utils/skillDetailUtils'; +import { DetailIcon } from './DetailIcon'; + +export type InstallPanelKey = 'agent' | 'human' | null; + +const relatedSkillIconUrls = [relatedSkillSqlIcon, relatedSkillSecurityIcon, relatedSkillDocsIcon]; +const relatedSkillShellClasses = ['is-blue', 'is-green', 'is-orange']; +const browseMarketPath = '/page/skills'; +const cliInstallPlaceholderCommand = + '# 待补齐 Doraemon CLI 安装脚本 URL,例如 curl -fsSL | bash'; + +interface SkillInstallPanelProps { + slug: string; + history: SkillDetailHistory; + related: SkillItem[]; + fileContent: SkillFileContent | null; + activeInstallPanel: InstallPanelKey; + onActiveInstallPanelChange: (panel: InstallPanelKey) => void; + isInstallable: boolean; + skillInstallCommand: string; + downloadCommand: string; + agentTerminalCommand: string; + manualDownloadUrl: string; +} + +const renderInlineCommand = (command: string, copyMessage: string, compact = false) => ( +
+
+
+ {command || '暂无可复制命令'} +
+
+
+); + +const renderTerminalCommand = (command: string, copyMessage: string) => ( +
+
+
+ + + +
+ BASH +
+
+ $ + {command || '暂无可复制命令'} +
+
+); + +export const SkillInstallPanel: React.FC = ({ + slug, + history, + related, + fileContent, + activeInstallPanel, + onActiveInstallPanelChange, + isInstallable, + skillInstallCommand, + downloadCommand, + agentTerminalCommand, + manualDownloadUrl, +}) => ( + +); diff --git a/app/web/pages/skills/detail/style.scss b/app/web/pages/skills/detail/style.scss index 5eb1e073..1f1b3110 100644 --- a/app/web/pages/skills/detail/style.scss +++ b/app/web/pages/skills/detail/style.scss @@ -7,7 +7,7 @@ background: #f7f9fb; font-family: var(--skill-detail-font-body); color: #2a3439; - .skill-detail-figma-icon { + .skill-detail-icon { display: inline-block; object-fit: contain; flex: 0 0 auto; diff --git a/app/web/pages/skills/detail/utils/skillDetailUtils.ts b/app/web/pages/skills/detail/utils/skillDetailUtils.ts new file mode 100644 index 00000000..8029ef78 --- /dev/null +++ b/app/web/pages/skills/detail/utils/skillDetailUtils.ts @@ -0,0 +1,156 @@ +import type { DataNode } from 'antd/lib/tree'; + +export interface SkillTreeNode extends DataNode { + children?: SkillTreeNode[]; +} + +export interface FrontmatterItem { + key: string; + value: string; +} + +export interface SkillDetailHistory { + push: (path: string) => void; +} + +export const formatFileSize = (size = 0) => { + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; + return `${(size / 1024 / 1024).toFixed(1)} MB`; +}; + +const sortTreeNodes = (nodes: SkillTreeNode[]) => { + nodes.sort((a, b) => { + const aIsLeaf = Boolean(a.isLeaf); + const bIsLeaf = Boolean(b.isLeaf); + if (aIsLeaf !== bIsLeaf) return aIsLeaf ? 1 : -1; + return String(a.title).localeCompare(String(b.title)); + }); + + nodes.forEach((node) => { + if (node.children && node.children.length > 0) { + sortTreeNodes(node.children); + } + }); +}; + +export const buildFileTreeData = (fileList: string[]): SkillTreeNode[] => { + const treeData: SkillTreeNode[] = []; + + fileList.forEach((filePath) => { + const segments = filePath.split('/').filter(Boolean); + let currentNodes = treeData; + let currentPath = ''; + + segments.forEach((segment, index) => { + currentPath = currentPath ? `${currentPath}/${segment}` : segment; + const isLeaf = index === segments.length - 1; + let node = currentNodes.find((item) => item.key === currentPath); + + if (!node) { + node = { + key: currentPath, + title: segment, + isLeaf, + children: isLeaf ? undefined : [], + }; + currentNodes.push(node); + } + + if (!isLeaf) { + node.children = node.children || []; + currentNodes = node.children; + } + }); + }); + + sortTreeNodes(treeData); + return treeData; +}; + +export const normalizeSourceUrl = (sourceRepo: string) => { + if (!sourceRepo) return ''; + const normalized = sourceRepo.replace(/^git\+/, '').trim(); + const sshMatch = normalized.match(/^git@([^:]+):(.+?)(?:\.git)?$/); + if (sshMatch) { + return `https://${sshMatch[1]}/${sshMatch[2]}`; + } + if (/^https?:\/\//.test(normalized)) { + return normalized.replace(/\.git$/, ''); + } + return ''; +}; + +const normalizeFrontmatterValue = (value: string) => { + const trimmed = String(value || '').trim(); + if (!trimmed) return '-'; + if ( + (trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'")) + ) { + return trimmed.slice(1, -1); + } + return trimmed; +}; + +export const parseMarkdownFrontmatter = ( + markdown = '' +): { frontmatter: FrontmatterItem[]; body: string } => { + const content = String(markdown || ''); + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!match) { + return { + frontmatter: [], + body: content, + }; + } + + const block = match[1] || ''; + const lines = block.split(/\r?\n/); + const frontmatter: FrontmatterItem[] = []; + let currentKey = ''; + let currentValueLines: string[] = []; + + const pushCurrent = () => { + if (!currentKey) return; + frontmatter.push({ + key: currentKey, + value: normalizeFrontmatterValue(currentValueLines.join('\n')), + }); + }; + + lines.forEach((line) => { + const keyMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/); + const isTopLevelKey = Boolean(keyMatch) && !/^\s/.test(line); + + if (isTopLevelKey && keyMatch) { + pushCurrent(); + currentKey = keyMatch[1]; + currentValueLines = [keyMatch[2] || '']; + return; + } + + if (currentKey) { + currentValueLines.push(line); + } + }); + + pushCurrent(); + + return { + frontmatter, + body: content.slice(match[0].length), + }; +}; + +export const formatDownloadCommand = (downloadUrl = '', fileName = 'skill.zip') => { + if (!downloadUrl) return ''; + return `curl -L "${downloadUrl}" -o ${fileName}`; +}; + +export const formatCompactDate = (value?: string) => { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return '-'; + return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; +}; From f88128da75e5cac74444a833544887b7b535418f Mon Sep 17 00:00:00 2001 From: huaiju Date: Sun, 14 Jun 2026 22:37:45 +0800 Subject: [PATCH 04/13] refactor: remove registry authentication flows --- dt-skill/src/browserAuth.test.ts | 96 - dt-skill/src/browserAuth.ts | 174 -- dt-skill/src/cli.ts | 196 +- dt-skill/src/cli/authToken.ts | 13 - dt-skill/src/cli/commands/auth.test.ts | 56 - dt-skill/src/cli/commands/auth.ts | 145 - dt-skill/src/cli/commands/delete.test.ts | 4 - .../src/cli/commands/moderationPlan.test.ts | 41 - dt-skill/src/cli/commands/moderationPlan.ts | 64 - dt-skill/src/cli/commands/ownership.test.ts | 76 - dt-skill/src/cli/commands/ownership.ts | 113 - dt-skill/src/cli/commands/packages.test.ts | 2497 ----------------- dt-skill/src/cli/commands/packages.ts | 1087 +------ dt-skill/src/cli/commands/publish.test.ts | 8 +- dt-skill/src/cli/commands/publishers.test.ts | 68 - dt-skill/src/cli/commands/publishers.ts | 47 - .../src/cli/commands/skills.install.test.ts | 14 +- dt-skill/src/cli/commands/skills.test.ts | 117 +- dt-skill/src/cli/commands/skills.ts | 176 +- dt-skill/src/cli/commands/sync.test.ts | 19 +- dt-skill/src/cli/commands/syncHelpers.ts | 7 +- dt-skill/src/cli/commands/transfer.test.ts | 132 - dt-skill/src/cli/commands/transfer.ts | 217 -- dt-skill/src/cli/registry.test.ts | 6 +- dt-skill/src/cli/registry.ts | 2 +- dt-skill/src/config.test.ts | 4 +- dt-skill/src/config.ts | 1 - dt-skill/src/deviceAuth.test.ts | 151 - dt-skill/src/deviceAuth.ts | 151 - dt-skill/src/discovery.test.ts | 3 - dt-skill/src/discovery.ts | 1 - dt-skill/src/homedir.ts | 2 +- dt-skill/src/http.bun.test.ts | 6 +- dt-skill/src/http.test.ts | 20 +- dt-skill/src/http.ts | 33 +- dt-skill/src/schema/packages.ts | 13 - dt-skill/src/schema/routes.ts | 3 - dt-skill/src/schema/schemas.ts | 9 - dt-skill/test/cliCommandTestKit.ts | 14 - 39 files changed, 71 insertions(+), 5715 deletions(-) delete mode 100644 dt-skill/src/browserAuth.test.ts delete mode 100644 dt-skill/src/browserAuth.ts delete mode 100644 dt-skill/src/cli/authToken.ts delete mode 100644 dt-skill/src/cli/commands/auth.test.ts delete mode 100644 dt-skill/src/cli/commands/auth.ts delete mode 100644 dt-skill/src/cli/commands/moderationPlan.test.ts delete mode 100644 dt-skill/src/cli/commands/moderationPlan.ts delete mode 100644 dt-skill/src/cli/commands/ownership.test.ts delete mode 100644 dt-skill/src/cli/commands/ownership.ts delete mode 100644 dt-skill/src/cli/commands/packages.test.ts delete mode 100644 dt-skill/src/cli/commands/publishers.test.ts delete mode 100644 dt-skill/src/cli/commands/publishers.ts delete mode 100644 dt-skill/src/cli/commands/transfer.test.ts delete mode 100644 dt-skill/src/cli/commands/transfer.ts delete mode 100644 dt-skill/src/deviceAuth.test.ts delete mode 100644 dt-skill/src/deviceAuth.ts diff --git a/dt-skill/src/browserAuth.test.ts b/dt-skill/src/browserAuth.test.ts deleted file mode 100644 index 30d6dd58..00000000 --- a/dt-skill/src/browserAuth.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* @vitest-environment node */ - -import { describe, expect, it } from "vitest"; -import { - buildCliAuthUrl, - isAllowedLoopbackRedirectUri, - startLoopbackAuthServer, -} from "./browserAuth"; - -describe("browserAuth", () => { - it("builds auth url", () => { - const url = buildCliAuthUrl({ - siteUrl: "https://example.com", - redirectUri: "http://127.0.0.1:1234/callback", - label: "CLI token", - state: "state123", - }); - expect(url).toContain("https://example.com/cli/auth?"); - expect(url).toContain("redirect_uri="); - expect(url).toContain("label_b64="); - expect(url).toContain("state="); - }); - - it("builds auth url without label", () => { - const url = buildCliAuthUrl({ - siteUrl: "https://example.com", - redirectUri: "http://127.0.0.1:1234/callback", - state: "state123", - }); - expect(url).toContain("https://example.com/cli/auth?"); - expect(url).not.toContain("label_b64="); - }); - - it("accepts only loopback http redirect uris", () => { - expect(isAllowedLoopbackRedirectUri("http://127.0.0.1:1234/callback")).toBe(true); - expect(isAllowedLoopbackRedirectUri("http://localhost:1234/callback")).toBe(true); - expect(isAllowedLoopbackRedirectUri("http://[::1]:1234/callback")).toBe(true); - expect(isAllowedLoopbackRedirectUri("https://127.0.0.1:1234/callback")).toBe(false); - expect(isAllowedLoopbackRedirectUri("http://evil.com/callback")).toBe(false); - expect(isAllowedLoopbackRedirectUri("not a url")).toBe(false); - }); - - it("receives token via loopback server", async () => { - const server = await startLoopbackAuthServer({ timeoutMs: 2000 }); - const payload = { - token: "clh_test", - registry: "https://example.convex.site", - state: server.state, - }; - await fetch(server.redirectUri.replace("/callback", "/token"), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - await expect(server.waitForResult()).resolves.toEqual(payload); - }); - - it("serves callback html", async () => { - const server = await startLoopbackAuthServer({ timeoutMs: 2000 }); - const response = await fetch(server.redirectUri); - expect(response.status).toBe(200); - const text = await response.text(); - expect(text).toContain("ClawHub CLI Login"); - server.close(); - }); - - it("returns 404 for unknown routes", async () => { - const server = await startLoopbackAuthServer({ timeoutMs: 2000 }); - const response = await fetch(server.redirectUri.replace("/callback", "/nope")); - expect(response.status).toBe(404); - server.close(); - }); - - it("rejects invalid json payloads", async () => { - const server = await startLoopbackAuthServer({ timeoutMs: 2000 }); - const tokenUrl = server.redirectUri.replace("/callback", "/token"); - const response = await fetch(tokenUrl, { method: "POST", body: "{" }); - expect(response.status).toBe(400); - await expect(server.waitForResult()).rejects.toThrow(); - }); - - it("rejects state mismatches", async () => { - const server = await startLoopbackAuthServer({ timeoutMs: 2000 }); - await fetch(server.redirectUri.replace("/callback", "/token"), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token: "clh_test", registry: "https://example.com", state: "nope" }), - }); - await expect(server.waitForResult()).rejects.toThrow(/state mismatch/i); - }); - - it("times out waiting for login", async () => { - const server = await startLoopbackAuthServer({ timeoutMs: 25 }); - await expect(server.waitForResult()).rejects.toThrow(/timed out waiting for browser login/i); - }); -}); diff --git a/dt-skill/src/browserAuth.ts b/dt-skill/src/browserAuth.ts deleted file mode 100644 index d412b30e..00000000 --- a/dt-skill/src/browserAuth.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { createServer } from "node:http"; -import type { AddressInfo } from "node:net"; - -type LoopbackAuthResult = { - token: string; - registry?: string; - state?: string; -}; - -export function buildCliAuthUrl(params: { - siteUrl: string; - redirectUri: string; - label?: string; - state: string; -}) { - const url = new URL("/cli/auth", params.siteUrl); - url.searchParams.set("redirect_uri", params.redirectUri); - if (params.label) url.searchParams.set("label_b64", encodeBase64Url(params.label)); - url.searchParams.set("state", params.state); - return url.toString(); -} - -export function isAllowedLoopbackRedirectUri(value: string) { - let url: URL; - try { - url = new URL(value); - } catch { - return false; - } - if (url.protocol !== "http:") return false; - const host = url.hostname.toLowerCase(); - if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1" && host !== "[::1]") { - return false; - } - return true; -} - -export async function startLoopbackAuthServer(params?: { timeoutMs?: number }) { - const timeoutMs = params?.timeoutMs ?? 5 * 60_000; - const expectedState = generateState(); - - let resolveToken: ((value: LoopbackAuthResult) => void) | null = null; - let rejectToken: ((error: Error) => void) | null = null; - const tokenPromise = new Promise((resolve, reject) => { - resolveToken = resolve; - rejectToken = reject; - }); - - const server = createServer((req, res) => { - const method = req.method ?? "GET"; - const url = req.url ?? "/"; - - if (method === "GET" && (url === "/" || url.startsWith("/callback"))) { - res.statusCode = 200; - res.setHeader("Content-Type", "text/html; charset=utf-8"); - res.end(CALLBACK_HTML); - return; - } - - if (method === "POST" && url === "/token") { - const chunks: Uint8Array[] = []; - req.on("data", (chunk) => chunks.push(chunk as Uint8Array)); - req.on("end", () => { - try { - const raw = Buffer.concat(chunks).toString("utf8"); - const parsed = JSON.parse(raw) as unknown; - if (!parsed || typeof parsed !== "object") throw new Error("invalid payload"); - const token = (parsed as { token?: unknown }).token; - const registry = (parsed as { registry?: unknown }).registry; - const state = (parsed as { state?: unknown }).state; - if (typeof token !== "string" || !token.trim()) throw new Error("token required"); - if (typeof state !== "string" || state !== expectedState) { - throw new Error("state mismatch"); - } - res.statusCode = 200; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ ok: true })); - resolveToken?.({ - token: token.trim(), - registry: typeof registry === "string" ? registry : undefined, - state, - }); - } catch (error) { - res.statusCode = 400; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ ok: false })); - const message = error instanceof Error ? error.message : "invalid payload"; - rejectToken?.(new Error(message)); - } finally { - server.close(); - } - }); - return; - } - - res.statusCode = 404; - res.setHeader("Content-Type", "text/plain; charset=utf-8"); - res.end("Not found"); - }); - - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", () => resolve()); - }); - const address = server.address() as AddressInfo | null; - if (!address) { - server.close(); - throw new Error("Failed to bind loopback server"); - } - const redirectUri = `http://127.0.0.1:${address.port}/callback`; - - const timeout = setTimeout(() => { - server.close(); - rejectToken?.(new Error("Timed out waiting for browser login")); - }, timeoutMs); - tokenPromise.finally(() => clearTimeout(timeout)).catch(() => {}); - - return { - redirectUri, - state: expectedState, - waitForResult: () => tokenPromise, - close: () => server.close(), - }; -} - -const CALLBACK_HTML = ` - - - - ClawHub CLI Login - - -
-

Completing login…

-

Waiting for token.

-
- - -`; - -function encodeBase64Url(value: string) { - return Buffer.from(value, "utf8").toString("base64url"); -} - -function generateState() { - return Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("hex"); -} diff --git a/dt-skill/src/cli.ts b/dt-skill/src/cli.ts index e00ea676..81eb73bb 100644 --- a/dt-skill/src/cli.ts +++ b/dt-skill/src/cli.ts @@ -11,25 +11,16 @@ import { cmdUnhideSkill, } from "./cli/commands/delete.js"; import { cmdInspect } from "./cli/commands/inspect.js"; -import { cmdMergeSkill, cmdRenameSkill } from "./cli/commands/ownership.js"; import { - cmdDeletePackage, cmdDownloadPackage, cmdExplorePackages, - cmdGetPackageTrustedPublisher, cmdInspectPackage, - cmdPackageModerationStatus, cmdPackageMigrationStatus, cmdPackageReadiness, cmdPackPackage, - cmdPublishPackage, - cmdReportPackage, - cmdTransferPackage, - cmdUndeletePackage, cmdVerifyPackage, } from "./cli/commands/packages.js"; import { cmdPublish } from "./cli/commands/publish.js"; -import { cmdCreatePublisher } from "./cli/commands/publishers.js"; import { cmdExplore, cmdInstall, @@ -42,13 +33,6 @@ import { } from "./cli/commands/skills.js"; import { cmdStarSkill } from "./cli/commands/star.js"; import { cmdSync } from "./cli/commands/sync.js"; -import { - cmdTransferAccept, - cmdTransferCancel, - cmdTransferList, - cmdTransferReject, - cmdTransferRequest, -} from "./cli/commands/transfer.js"; import { cmdUnstarSkill } from "./cli/commands/unstar.js"; import { isAgentName, listAgentNames, resolveAgentWorkdir } from "./cli/agents.js"; import { configureCommanderHelp, styleEnvBlock, styleTitle } from "./cli/helpStyle.js"; @@ -361,23 +345,8 @@ registerCommand(skill, ["skill", "publish"]) await cmdPublish(opts, folder, options); }); -const publisherCmd = registerCommandGroup(program, ["publisher"]) - .description("Publisher organization commands") - .showHelpAfterError() - .showSuggestionAfterError(); - -registerCommand(publisherCmd, ["publisher", "create"]) - .description("Create an org publisher you own") - .argument("", "Publisher handle, for example opik") - .option("--display-name ", "Publisher display name") - .option("--json", "Output JSON") - .action(async (handle, options) => { - const opts = await resolveGlobalOpts(); - await cmdCreatePublisher(opts, handle, options); - }); - const packageCmd = registerCommandGroup(program, ["package"]).description( - "Browse and publish OpenClaw packages", + "Browse OpenClaw packages", ); registerCommand(packageCmd, ["package", "explore"]) @@ -458,57 +427,6 @@ registerCommand(packageCmd, ["package", "verify"]) }); }); -registerCommand(packageCmd, ["package", "delete"]) - .description("Soft-delete a package and all releases") - .argument("", "Package name") - .option("--yes", "Skip confirmation") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdDeletePackage(opts, name, options, isInputAllowed()); - }); - -registerCommand(packageCmd, ["package", "undelete"]) - .description("Restore a soft-deleted package and releases") - .argument("", "Package name") - .option("--yes", "Skip confirmation") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdUndeletePackage(opts, name, options, isInputAllowed()); - }); - -registerCommand(packageCmd, ["package", "transfer"]) - .description("Transfer a plugin package to another publisher") - .argument("", "Package name") - .requiredOption("--to ", "Destination publisher handle") - .option("--reason ", "Audit reason") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdTransferPackage(opts, name, options); - }); - -registerCommand(packageCmd, ["package", "report"]) - .description("Report a package for moderator review") - .argument("", "Package name") - .option("--version ", "Package version") - .requiredOption("--reason ", "Report reason") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdReportPackage(opts, name, options); - }); - -registerCommand(packageCmd, ["package", "moderation-status"]) - .description("Show package moderation status") - .argument("", "Package name") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdPackageModerationStatus(opts, name, options); - }); - registerCommand(packageCmd, ["package", "readiness"]) .description("Check package readiness for future OpenClaw consumption") .argument("", "Package name") @@ -537,118 +455,6 @@ registerCommand(packageCmd, ["package", "pack"]) await cmdPackPackage(opts, source, options); }); -registerCommand(packageCmd, ["package", "publish"]) - .description("Publish a code plugin or bundle plugin from a folder or GitHub source") - .argument("", "Package folder path, GitHub repo (owner/repo[@ref]), or URL") - .option("--family ", "code-plugin|bundle-plugin") - .option("--name ", "Package name") - .option("--display-name ", "Display name") - .option("--owner ", "Publish under this owner/publisher handle") - .option("--version ", "Version") - .option("--changelog ", "Changelog text") - .option("--clawscan-note ", CLAWSCAN_NOTE_HELP) - .option( - "--manual-override-reason ", - "Required for manual publish when trusted publisher config exists", - ) - .option("--tags ", "Comma-separated tags", "latest") - .option("--bundle-format ", "Bundle format") - .option("--host-targets ", "Comma-separated bundle host targets") - .option("--source-repo ", "GitHub repo (owner/repo or URL)") - .option("--source-commit ", "Git commit SHA") - .option("--source-ref ", "Git ref/tag/branch") - .option("--source-path ", "Repo subpath") - .option("--dry-run", "Preview what would be published without uploading") - .option("--json", "Output JSON (for CI pipelines)") - .action(async (source, options) => { - const opts = await resolveGlobalOpts(); - await cmdPublishPackage(opts, source, options); - }); - -const trustedPublisherCmd = registerCommandGroup(packageCmd, [ - "package", - "trusted-publisher", -]).description("Manage package trusted publisher config"); - -registerCommand(trustedPublisherCmd, ["package", "trusted-publisher", "get"]) - .description("Show trusted publisher config for a package") - .argument("", "Package name") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdGetPackageTrustedPublisher(opts, name, options); - }); - -registerCommand(skill, ["skill", "rename"]) - .description("Rename a published skill and keep the old slug as a redirect") - .argument("", "Current skill slug") - .argument("", "New canonical slug") - .option("--yes", "Skip confirmation") - .action(async (slug, newSlug, options) => { - const opts = await resolveGlobalOpts(); - await cmdRenameSkill(opts, slug, newSlug, options, isInputAllowed()); - }); - -registerCommand(skill, ["skill", "merge"]) - .description("Merge one owned skill into another and redirect the old slug") - .argument("", "Source skill slug") - .argument("", "Target canonical slug") - .option("--yes", "Skip confirmation") - .action(async (sourceSlug, targetSlug, options) => { - const opts = await resolveGlobalOpts(); - await cmdMergeSkill(opts, sourceSlug, targetSlug, options, isInputAllowed()); - }); - -const transfer = registerCommandGroup(program, ["transfer"]).description( - "Transfer skill ownership", -); - -registerCommand(transfer, ["transfer", "request"]) - .description("Request skill transfer to another user") - .argument("", "Skill slug") - .argument("", "Recipient handle (e.g., @username)") - .option("--message ", "Optional message for recipient") - .option("--yes", "Skip confirmation") - .action(async (slug, handle, options) => { - const opts = await resolveGlobalOpts(); - await cmdTransferRequest(opts, slug, handle, options, isInputAllowed()); - }); - -registerCommand(transfer, ["transfer", "list"]) - .description("List pending transfer requests") - .option("--outgoing", "Show outgoing transfer requests") - .action(async (options) => { - const opts = await resolveGlobalOpts(); - await cmdTransferList(opts, options); - }); - -registerCommand(transfer, ["transfer", "accept"]) - .description("Accept incoming transfer for a skill") - .argument("", "Skill slug") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdTransferAccept(opts, slug, options, isInputAllowed()); - }); - -registerCommand(transfer, ["transfer", "reject"]) - .description("Reject incoming transfer for a skill") - .argument("", "Skill slug") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdTransferReject(opts, slug, options, isInputAllowed()); - }); - -registerCommand(transfer, ["transfer", "cancel"]) - .description("Cancel outgoing transfer for a skill") - .argument("", "Skill slug") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdTransferCancel(opts, slug, options, isInputAllowed()); - }); - registerCommand(program, ["star"]) .description("Add a skill to your highlights") .argument("", "Skill slug") diff --git a/dt-skill/src/cli/authToken.ts b/dt-skill/src/cli/authToken.ts deleted file mode 100644 index 19591e78..00000000 --- a/dt-skill/src/cli/authToken.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { readGlobalConfig } from "../config.js"; -import { fail } from "./ui.js"; - -export async function getOptionalAuthToken(): Promise { - const cfg = await readGlobalConfig(); - return cfg?.token ?? undefined; -} - -export async function requireAuthToken(): Promise { - const token = await getOptionalAuthToken(); - if (!token) fail("Not logged in. Run: clawhub login"); - return token; -} diff --git a/dt-skill/src/cli/commands/auth.test.ts b/dt-skill/src/cli/commands/auth.test.ts deleted file mode 100644 index 749fa530..00000000 --- a/dt-skill/src/cli/commands/auth.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* @vitest-environment node */ - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { createRegistryModuleMocks, makeGlobalOpts } from "../../../test/cliCommandTestKit.js"; - -const mockReadGlobalConfig = vi.fn( - async () => null as { registry?: string; token?: string } | null, -); -const mockWriteGlobalConfig = vi.fn(async (_cfg: unknown) => {}); -vi.mock("../../config.js", () => ({ - readGlobalConfig: () => mockReadGlobalConfig(), - writeGlobalConfig: (cfg: unknown) => mockWriteGlobalConfig(cfg), -})); - -const registryMocks = createRegistryModuleMocks(); -const mockGetRegistry = registryMocks.getRegistry; -vi.mock("../registry.js", () => registryMocks.moduleFactory()); - -const { cmdLogout } = await import("./auth"); - -const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); - -afterEach(() => { - vi.clearAllMocks(); - mockLog.mockClear(); -}); - -describe("cmdLogout", () => { - it("removes token and logs a clear message", async () => { - mockReadGlobalConfig.mockResolvedValueOnce({ registry: "https://clawhub.ai", token: "tkn" }); - - await cmdLogout(makeGlobalOpts()); - - expect(mockWriteGlobalConfig).toHaveBeenCalledWith({ - registry: "https://clawhub.ai", - token: undefined, - }); - expect(mockGetRegistry).not.toHaveBeenCalled(); - expect(mockLog).toHaveBeenCalledWith( - "OK. Logged out locally. Token still valid until revoked (Settings -> API tokens).", - ); - }); - - it("falls back to resolved registry when config has no registry", async () => { - mockReadGlobalConfig.mockResolvedValueOnce({ token: "tkn" }); - mockGetRegistry.mockResolvedValueOnce("https://registry.example"); - - await cmdLogout(makeGlobalOpts()); - - expect(mockGetRegistry).toHaveBeenCalled(); - expect(mockWriteGlobalConfig).toHaveBeenCalledWith({ - registry: "https://registry.example", - token: undefined, - }); - }); -}); diff --git a/dt-skill/src/cli/commands/auth.ts b/dt-skill/src/cli/commands/auth.ts deleted file mode 100644 index 0ea311fd..00000000 --- a/dt-skill/src/cli/commands/auth.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { buildCliAuthUrl, startLoopbackAuthServer } from "../../browserAuth.js"; -import { readGlobalConfig, writeGlobalConfig } from "../../config.js"; -import { pollForDeviceToken, requestDeviceCode } from "../../deviceAuth.js"; -import { discoverRegistryFromSite } from "../../discovery.js"; -import { apiRequest } from "../../http.js"; -import { ApiRoutes, ApiV1WhoamiResponseSchema } from "../../schema/index.js"; -import { requireAuthToken } from "../authToken.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, openInBrowser, promptHidden } from "../ui.js"; - -export async function cmdLoginFlow( - opts: GlobalOpts, - options: { token?: string; label?: string; browser?: boolean; device?: boolean }, - inputAllowed: boolean, -) { - if (options.token) { - await cmdLogin(opts, options.token, inputAllowed); - return; - } - - if (options.device) { - await cmdDeviceLogin(opts); - return; - } - - if (options.browser === false) { - fail("Token required (use --token, --device, or remove --no-browser)"); - } - - const label = (options.label ?? "CLI token").trim() || "CLI token"; - const receiver = await startLoopbackAuthServer(); - const discovery = await discoverRegistryFromSite(opts.site).catch(() => null); - const authBase = discovery?.authBase?.trim() || opts.site; - const authUrl = buildCliAuthUrl({ - siteUrl: authBase, - redirectUri: receiver.redirectUri, - label, - state: receiver.state, - }); - - console.log(`Opening browser: ${authUrl}`); - openInBrowser(authUrl); - - const result = await receiver.waitForResult(); - const registry = result.registry?.trim() || opts.registry; - const registrySource = result.registry?.trim() ? "cli" : opts.registrySource; - await cmdLogin({ ...opts, registry, registrySource }, result.token, inputAllowed); -} - -async function cmdLogin(opts: GlobalOpts, tokenFlag: string | undefined, inputAllowed: boolean) { - if (!tokenFlag && !inputAllowed) fail("Token required (use --token or remove --no-input)"); - - const token = tokenFlag || (await promptHidden("ClawHub token: ")); - if (!token) fail("Token required"); - - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Verifying token"); - try { - const whoami = await apiRequest( - registry, - { method: "GET", path: ApiRoutes.whoami, token }, - ApiV1WhoamiResponseSchema, - ); - if (!whoami.user) fail("Login failed"); - - await writeGlobalConfig({ registry, token }); - const handle = whoami.user.handle ? `@${whoami.user.handle}` : "unknown user"; - spinner.succeed(`OK. Logged in as ${handle}.`); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdLogout(opts: GlobalOpts) { - const cfg = await readGlobalConfig(); - const registry = cfg?.registry || (await getRegistry(opts, { cache: true })); - await writeGlobalConfig({ registry, token: undefined }); - console.log("OK. Logged out locally. Token still valid until revoked (Settings -> API tokens)."); -} - -export async function cmdWhoami(opts: GlobalOpts) { - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - - const spinner = createSpinner("Checking token"); - try { - const whoami = await apiRequest( - registry, - { method: "GET", path: ApiRoutes.whoami, token }, - ApiV1WhoamiResponseSchema, - ); - spinner.succeed(whoami.user.handle ?? "unknown"); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -/** - * Device Flow login for headless environments. - * Requests a device code, displays it to the user, then polls until authorized. - */ -export async function cmdDeviceLogin(opts: GlobalOpts) { - const discovery = await discoverRegistryFromSite(opts.site).catch(() => null); - const authBase = discovery?.authBase?.trim() || opts.site; - const registry = await getRegistry(opts, { cache: true }); - - const spinner = createSpinner("Requesting device code"); - let deviceCode; - try { - deviceCode = await requestDeviceCode({ apiUrl: registry, siteUrl: authBase }); - spinner.succeed("Device code received"); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } - - // Display the code and URL for the user - console.log(); - console.log(" To authenticate, visit:"); - console.log(` ${deviceCode.verification_uri}`); - console.log(); - console.log(` And enter code: ${deviceCode.user_code}`); - console.log(); - console.log(` Code expires in ${Math.floor(deviceCode.expires_in / 60)} minutes.`); - console.log(); - - const pollSpinner = createSpinner("Waiting for authorization"); - try { - const tokenResponse = await pollForDeviceToken( - { apiUrl: registry, siteUrl: authBase }, - deviceCode.device_code, - { interval: deviceCode.interval, expiresIn: deviceCode.expires_in }, - ); - pollSpinner.succeed("Authorized"); - - // Store the token - await cmdLogin({ ...opts, registry, registrySource: "cli" }, tokenResponse.access_token, true); - } catch (error) { - pollSpinner.fail(formatError(error)); - throw error; - } -} diff --git a/dt-skill/src/cli/commands/delete.test.ts b/dt-skill/src/cli/commands/delete.test.ts index feca4060..5899d6b9 100644 --- a/dt-skill/src/cli/commands/delete.test.ts +++ b/dt-skill/src/cli/commands/delete.test.ts @@ -2,19 +2,16 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { - createAuthTokenModuleMocks, createHttpModuleMocks, createRegistryModuleMocks, createUiModuleMocks, makeGlobalOpts, } from "../../../test/cliCommandTestKit.js"; -const authTokenMocks = createAuthTokenModuleMocks(); const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks(); -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); vi.mock("../registry.js", () => registryMocks.moduleFactory()); vi.mock("../../http.js", () => httpMocks.moduleFactory()); vi.mock("../ui.js", () => uiMocks.moduleFactory()); @@ -41,7 +38,6 @@ describe("delete/undelete", () => { expect.not.objectContaining({ token: expect.anything() }), expect.anything(), ); - expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); }); it("prints the slug reservation expiry returned by delete", async () => { diff --git a/dt-skill/src/cli/commands/moderationPlan.test.ts b/dt-skill/src/cli/commands/moderationPlan.test.ts deleted file mode 100644 index 433a892d..00000000 --- a/dt-skill/src/cli/commands/moderationPlan.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* @vitest-environment node */ - -import { describe, expect, it } from "vitest"; -import { reportModerationPlan } from "./moderationPlan"; - -describe("moderation plan summaries", () => { - it.each([ - { - name: "confirmed skill report with hide", - plan: reportModerationPlan({ - entityLabel: "skill", - reportId: "skillReports:1", - status: "confirmed", - finalAction: "hide", - }), - expected: { - subject: "skill report skillReports:1", - outcome: "set status to confirmed; final action hide", - impacts: ["Mark the report as confirmed.", "Hide the skill from public availability."], - requiresConfirmation: true, - }, - }, - { - name: "dismissed package report with no final action", - plan: reportModerationPlan({ - entityLabel: "package", - reportId: "packageReports:1", - status: "dismissed", - finalAction: "none", - }), - expected: { - subject: "package report packageReports:1", - outcome: "set status to dismissed; final action none", - impacts: ["Dismiss the report without changing artifact availability."], - requiresConfirmation: false, - }, - }, - ])("describes $name", ({ plan, expected }) => { - expect(plan).toMatchObject(expected); - }); -}); diff --git a/dt-skill/src/cli/commands/moderationPlan.ts b/dt-skill/src/cli/commands/moderationPlan.ts deleted file mode 100644 index fbeba562..00000000 --- a/dt-skill/src/cli/commands/moderationPlan.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { fail, isInteractive, promptConfirm } from "../ui.js"; - -type ModerationPlanOptions = { - json?: boolean; - yes?: boolean; -}; - -type ModerationPlan = { - subject: string; - outcome: string; - impacts: string[]; - requiresConfirmation: boolean; - confirmPrompt: string; -}; - -export function reportModerationPlan(params: { - entityLabel: "skill" | "package"; - reportId: string; - status: "open" | "confirmed" | "dismissed"; - finalAction?: "none" | "hide" | "quarantine" | "revoke"; -}): ModerationPlan { - const impacts: string[] = []; - if (params.status === "open") { - impacts.push("Reopen the report for review."); - } else if (params.status === "confirmed") { - impacts.push("Mark the report as confirmed."); - } else { - impacts.push("Dismiss the report without changing artifact availability."); - } - - if (params.finalAction === "hide") { - impacts.push("Hide the skill from public availability."); - } else if (params.finalAction === "quarantine") { - impacts.push("Quarantine the package release."); - } else if (params.finalAction === "revoke") { - impacts.push("Revoke the package release."); - } - - const action = params.finalAction && params.finalAction !== "none" ? params.finalAction : "none"; - return { - subject: `${params.entityLabel} report ${params.reportId}`, - outcome: `set status to ${params.status}; final action ${action}`, - impacts, - requiresConfirmation: action !== "none", - confirmPrompt: `Apply this ${params.entityLabel} report action?`, - }; -} - -export async function presentModerationPlan(plan: ModerationPlan, options: ModerationPlanOptions) { - if (!options.json) { - console.log("Moderation action summary"); - console.log(` case: ${plan.subject}`); - console.log(` outcome: ${plan.outcome}`); - console.log(" public impact:"); - for (const impact of plan.impacts) { - console.log(` - ${impact}`); - } - } - - if (!plan.requiresConfirmation || options.yes) return; - if (!isInteractive()) fail("Pass --yes (no input)"); - const confirmed = await promptConfirm(plan.confirmPrompt); - if (!confirmed) fail("Canceled"); -} diff --git a/dt-skill/src/cli/commands/ownership.test.ts b/dt-skill/src/cli/commands/ownership.test.ts deleted file mode 100644 index 85b7f20b..00000000 --- a/dt-skill/src/cli/commands/ownership.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* @vitest-environment node */ - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; - -const authTokenMocks = createAuthTokenModuleMocks(); -const registryMocks = createRegistryModuleMocks(); -const httpMocks = createHttpModuleMocks(); -const uiMocks = createUiModuleMocks(); - -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../ui.js", () => uiMocks.moduleFactory()); - -const { cmdMergeSkill, cmdRenameSkill } = await import("./ownership"); - -afterEach(() => { - vi.clearAllMocks(); -}); - -describe("ownership commands", () => { - it("rename requires --yes when input is disabled", async () => { - await expect(cmdRenameSkill(makeGlobalOpts(), "demo", "demo-new", {}, false)).rejects.toThrow( - /--yes/i, - ); - }); - - it("rename calls rename endpoint", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - slug: "demo-new", - previousSlug: "demo", - }); - - await cmdRenameSkill(makeGlobalOpts(), "Demo", "Demo-New", { yes: true }, false); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "POST", - path: "/api/v1/skills/demo/rename", - }), - expect.anything(), - ); - const requestArgs = httpMocks.apiRequest.mock.calls[0]?.[1] as { body?: unknown }; - expect(requestArgs.body).toEqual({ newSlug: "demo-new" }); - }); - - it("merge calls merge endpoint", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - sourceSlug: "demo-old", - targetSlug: "demo", - }); - - await cmdMergeSkill(makeGlobalOpts(), "Demo-Old", "Demo", { yes: true }, false); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "POST", - path: "/api/v1/skills/demo-old/merge", - }), - expect.anything(), - ); - const requestArgs = httpMocks.apiRequest.mock.calls[0]?.[1] as { body?: unknown }; - expect(requestArgs.body).toEqual({ targetSlug: "demo" }); - }); -}); diff --git a/dt-skill/src/cli/commands/ownership.ts b/dt-skill/src/cli/commands/ownership.ts deleted file mode 100644 index ba6a2df4..00000000 --- a/dt-skill/src/cli/commands/ownership.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { apiRequest } from "../../http.js"; -import { - ApiRoutes, - ApiV1SkillMergeResponseSchema, - ApiV1SkillRenameResponseSchema, - parseArk, -} from "../../schema/index.js"; -import { requireAuthToken } from "../authToken.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; - -type ConfirmOptions = { yes?: boolean }; - -function normalizeSlug(slugArg: string, label = "Skill slug") { - const slug = slugArg.trim().toLowerCase(); - if (!slug) fail(`${label} required`); - return slug; -} - -function canPrompt(inputAllowed: boolean) { - return isInteractive() && inputAllowed !== false; -} - -async function requireYesOrConfirm(options: ConfirmOptions, inputAllowed: boolean, prompt: string) { - if (options.yes) return true; - if (!canPrompt(inputAllowed)) fail("Pass --yes (no input)"); - return promptConfirm(prompt); -} - -export async function cmdRenameSkill( - opts: GlobalOpts, - slugArg: string, - newSlugArg: string, - options: ConfirmOptions, - inputAllowed: boolean, -) { - const slug = normalizeSlug(slugArg); - const newSlug = normalizeSlug(newSlugArg, "New slug"); - if (slug === newSlug) fail("New slug must be different"); - - const confirmed = await requireYesOrConfirm( - options, - inputAllowed, - `Rename ${slug} to ${newSlug}? Old slug will redirect.`, - ); - if (!confirmed) return undefined; - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Renaming ${slug} to ${newSlug}`); - - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/rename`, - token, - body: { newSlug }, - }, - ApiV1SkillRenameResponseSchema, - ); - const parsed = parseArk(ApiV1SkillRenameResponseSchema, result, "Rename skill response"); - spinner.succeed(`Renamed ${parsed.previousSlug} to ${parsed.slug}`); - return parsed; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdMergeSkill( - opts: GlobalOpts, - sourceSlugArg: string, - targetSlugArg: string, - options: ConfirmOptions, - inputAllowed: boolean, -) { - const sourceSlug = normalizeSlug(sourceSlugArg, "Source slug"); - const targetSlug = normalizeSlug(targetSlugArg, "Target slug"); - if (sourceSlug === targetSlug) fail("Target slug must be different"); - - const confirmed = await requireYesOrConfirm( - options, - inputAllowed, - `Merge ${sourceSlug} into ${targetSlug}? Source slug will redirect and stop listing publicly.`, - ); - if (!confirmed) return undefined; - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Merging ${sourceSlug} into ${targetSlug}`); - - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/${encodeURIComponent(sourceSlug)}/merge`, - token, - body: { targetSlug }, - }, - ApiV1SkillMergeResponseSchema, - ); - const parsed = parseArk(ApiV1SkillMergeResponseSchema, result, "Merge skill response"); - spinner.succeed(`Merged ${parsed.sourceSlug} into ${parsed.targetSlug}`); - return parsed; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} diff --git a/dt-skill/src/cli/commands/packages.test.ts b/dt-skill/src/cli/commands/packages.test.ts deleted file mode 100644 index e3f49549..00000000 --- a/dt-skill/src/cli/commands/packages.test.ts +++ /dev/null @@ -1,2497 +0,0 @@ -/* @vitest-environment node */ - -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { gzipSync, zipSync } from "fflate"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; -import { MAX_CLAWSCAN_NOTE_CHARS } from "../../schema/index.js"; - -const authTokenMocks = createAuthTokenModuleMocks(); -const registryMocks = createRegistryModuleMocks(); -const httpMocks = createHttpModuleMocks(); -const uiMocks = createUiModuleMocks(); -const originalOidcRequestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; -const originalOidcRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); -vi.mock("../ui.js", () => uiMocks.moduleFactory()); - -const { - cmdDeletePackage, - cmdDownloadPackage, - cmdExplorePackages, - cmdGetPackageTrustedPublisher, - cmdInspectPackage, - cmdPackageModerationStatus, - cmdPackageMigrationStatus, - cmdPackageReadiness, - cmdPackPackage, - cmdPublishPackage, - cmdReportPackage, - cmdTransferPackage, - cmdUndeletePackage, - cmdVerifyPackage, -} = await import("./packages"); -const { - cmdBackfillPackageArtifacts, - cmdDeletePackageTrustedPublisher, - cmdListPackageMigrations, - cmdListPackageReports, - cmdModeratePackageRelease, - cmdPackageModerationQueue, - cmdSetPackageTrustedPublisher, - cmdTriagePackageReport, - cmdUpsertPackageMigration, -} = await import("../../../../clawhub-mod/src/commands/packages"); -const { parseClawPack } = await import("../../clawpack"); - -const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); -const mockWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); - -function makeOpts(workdir = "/work") { - return makeGlobalOpts(workdir); -} - -async function makeTmpWorkdir() { - return await mkdtemp(join(tmpdir(), "clawhub-package-")); -} - -function runGit(cwd: string, args: string[]) { - const result = spawnSync("git", ["-C", cwd, ...args], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }); - if (result.status !== 0) { - throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); - } - return result.stdout.trim(); -} - -function getPublishForm() { - const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/v1/packages"; - }); - if (!publishCall) throw new Error("Missing publish call"); - const form = (publishCall[1] as { form?: FormData }).form; - if (!(form instanceof FormData)) throw new Error("Missing publish form"); - return form; -} - -function getPublishPayload() { - const form = getPublishForm(); - const payloadEntry = form.get("payload"); - if (typeof payloadEntry !== "string") throw new Error("Missing publish payload"); - return JSON.parse(payloadEntry) as Record; -} - -function getUploadedFileNames() { - const form = getPublishForm(); - return (form.getAll("files") as Array) - .map((file) => file.name ?? "") - .sort(); -} - -function getUploadedClawPackNames() { - const form = getPublishForm(); - return (form.getAll("clawpack") as Array) - .map((file) => file.name ?? "") - .sort(); -} - -function getUploadedClawPacks() { - const form = getPublishForm(); - return form.getAll("clawpack") as Array; -} - -function makeCodePluginPackageJson(overrides: Record) { - return JSON.stringify({ - openclaw: { - extensions: ["./dist/index.js"], - hostTargets: ["darwin-arm64", "linux-x64", "win32-x64"], - environment: {}, - compat: { - pluginApi: ">=2026.3.24-beta.2", - }, - build: { - openclawVersion: "2026.3.24-beta.2", - }, - }, - ...overrides, - }); -} - -const TAR_BLOCK_SIZE = 512; - -function writeTarString(target: Uint8Array, offset: number, width: number, value: string) { - const encoded = new TextEncoder().encode(value); - target.set(encoded.subarray(0, width), offset); -} - -function tarOctal(value: number, width: number) { - return value.toString(8).padStart(width - 1, "0") + "\0"; -} - -function tarFile(path: string, content: string) { - const bytes = new TextEncoder().encode(content); - const header = new Uint8Array(TAR_BLOCK_SIZE); - writeTarString(header, 0, 100, path); - writeTarString(header, 100, 8, tarOctal(0o644, 8)); - writeTarString(header, 108, 8, tarOctal(0, 8)); - writeTarString(header, 116, 8, tarOctal(0, 8)); - writeTarString(header, 124, 12, tarOctal(bytes.byteLength, 12)); - writeTarString(header, 136, 12, tarOctal(0, 12)); - header.fill(0x20, 148, 156); - header[156] = "0".charCodeAt(0); - writeTarString(header, 257, 6, "ustar"); - writeTarString(header, 263, 2, "00"); - - let checksum = 0; - for (const byte of header) checksum += byte; - writeTarString(header, 148, 8, tarOctal(checksum, 8)); - - const paddedSize = Math.ceil(bytes.byteLength / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; - const body = new Uint8Array(paddedSize); - body.set(bytes); - return [header, body]; -} - -function npmPackFixture(files: Record) { - const parts: Uint8Array[] = []; - for (const [path, content] of Object.entries(files)) { - parts.push(...tarFile(path, content)); - } - parts.push(new Uint8Array(TAR_BLOCK_SIZE), new Uint8Array(TAR_BLOCK_SIZE)); - const size = parts.reduce((sum, part) => sum + part.byteLength, 0); - const tar = new Uint8Array(size); - let offset = 0; - for (const part of parts) { - tar.set(part, offset); - offset += part.byteLength; - } - return gzipSync(tar); -} - -function artifactIdentity(bytes: Uint8Array) { - return { - sha256: createHash("sha256").update(bytes).digest("hex"), - npmIntegrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`, - npmShasum: createHash("sha1").update(bytes).digest("hex"), - }; -} - -afterEach(() => { - vi.clearAllMocks(); - mockLog.mockClear(); - mockWrite.mockClear(); - uiMocks.spinner.text = ""; - vi.unstubAllGlobals(); - if (originalOidcRequestUrl === undefined) { - delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL; - } else { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = originalOidcRequestUrl; - } - if (originalOidcRequestToken === undefined) { - delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - } else { - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = originalOidcRequestToken; - } -}); - -describe("package commands", () => { - it("searches package catalog via /api/v1/packages/search", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - results: [ - { - score: 10, - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - channel: "community", - isOfficial: false, - summary: "Demo plugin", - latestVersion: "1.2.3", - }, - }, - ], - }); - - await cmdExplorePackages(makeOpts(), "demo plugin", { - family: "code-plugin", - executesCode: true, - os: "darwin", - requiresBrowser: true, - externalService: "GitHub", - artifactKind: "npm-pack", - npmMirror: true, - }); - - const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; - const url = new URL(String(request?.url)); - expect(url.pathname).toBe("/api/v1/packages/search"); - expect(url.searchParams.get("q")).toBe("demo plugin"); - expect(url.searchParams.get("family")).toBe("code-plugin"); - expect(url.searchParams.get("executesCode")).toBe("true"); - expect(url.searchParams.get("os")).toBe("darwin"); - expect(url.searchParams.get("requiresBrowser")).toBe("true"); - expect(url.searchParams.get("externalService")).toBe("GitHub"); - expect(url.searchParams.get("artifactKind")).toBe("npm-pack"); - expect(url.searchParams.get("npmMirror")).toBe("true"); - }); - - it("supports skill family package browse requests", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - items: [], - nextCursor: null, - }); - - await cmdExplorePackages(makeOpts(), "", { family: "skill", target: "linux-x64", limit: 7 }); - - const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; - const url = new URL(String(request?.url)); - expect(url.pathname).toBe("/api/v1/packages"); - expect(url.searchParams.get("family")).toBe("skill"); - expect(url.searchParams.get("target")).toBe("linux-x64"); - expect(url.searchParams.get("limit")).toBe("7"); - }); - - it("uses tag param when fetching a package file", async () => { - httpMocks.apiRequest - .mockResolvedValueOnce({ - package: { - name: "demo", - displayName: "Demo", - family: "code-plugin", - runtimeId: "demo.plugin", - channel: "community", - isOfficial: false, - summary: null, - latestVersion: "2.0.0", - createdAt: 1, - updatedAt: 2, - tags: { latest: "2.0.0" }, - compatibility: null, - capabilities: { executesCode: true }, - verification: { - tier: "structural", - scope: "artifact-only", - }, - }, - owner: null, - }) - .mockResolvedValueOnce({ - package: { name: "demo", displayName: "Demo", family: "code-plugin" }, - version: { - version: "2.0.0", - createdAt: 3, - changelog: "init", - files: [], - }, - }); - httpMocks.fetchText.mockResolvedValue("content"); - - await cmdInspectPackage(makeOpts(), "demo", { file: "README.md", tag: "latest" }); - - const fetchArgs = httpMocks.fetchText.mock.calls[0]?.[1] as { url?: string } | undefined; - const url = new URL(String(fetchArgs?.url)); - expect(url.pathname).toBe("/api/v1/packages/demo/file"); - expect(url.searchParams.get("path")).toBe("README.md"); - expect(url.searchParams.get("tag")).toBe("latest"); - expect(url.searchParams.get("version")).toBeNull(); - }); - - it("downloads a ClawPack artifact through the explicit artifact resolver", async () => { - const workdir = await makeTmpWorkdir(); - try { - const bytes = npmPackFixture({ - "package/package.json": JSON.stringify({ - name: "@scope/demo", - version: "1.2.3", - }), - "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), - }); - const identity = artifactIdentity(bytes); - await mkdir(join(workdir, "downloads"), { recursive: true }); - httpMocks.apiRequest - .mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - runtimeId: "demo.plugin", - channel: "community", - isOfficial: false, - summary: null, - latestVersion: "1.2.3", - createdAt: 1, - updatedAt: 2, - tags: { latest: "1.2.3" }, - }, - owner: null, - }) - .mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - }, - version: "1.2.3", - artifact: { - kind: "npm-pack", - sha256: identity.sha256, - size: bytes.byteLength, - format: "tgz", - npmIntegrity: identity.npmIntegrity, - npmShasum: identity.npmShasum, - npmTarballName: "demo-1.2.3.tgz", - downloadUrl: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", - tarballUrl: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", - legacyDownloadUrl: - "https://clawhub.ai/api/v1/packages/@scope/demo/download?version=1.2.3", - }, - }); - httpMocks.fetchBinary.mockResolvedValue(bytes); - - await cmdDownloadPackage(makeOpts(workdir), "@scope/demo", { - tag: "latest", - output: "downloads", - }); - - expect(httpMocks.apiRequest.mock.calls[1]?.[1]).toMatchObject({ - method: "GET", - path: "/api/v1/packages/%40scope%2Fdemo/versions/1.2.3/artifact", - }); - expect(httpMocks.fetchBinary).toHaveBeenCalledWith("https://clawhub.ai", { - url: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", - token: undefined, - }); - expect(await readFile(join(workdir, "downloads", "demo-1.2.3.tgz"))).toEqual( - Buffer.from(bytes), - ); - expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Downloaded @scope/demo@1.2.3")); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("downloads legacy ZIP artifacts without enforcing stale stored release digests", async () => { - const workdir = await makeTmpWorkdir(); - try { - const bytes = new TextEncoder().encode("rebuilt legacy zip"); - await mkdir(join(workdir, "downloads"), { recursive: true }); - httpMocks.apiRequest - .mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - runtimeId: "demo.plugin", - channel: "community", - isOfficial: false, - summary: null, - latestVersion: "1.2.3", - createdAt: 1, - updatedAt: 2, - tags: { latest: "1.2.3" }, - }, - owner: null, - }) - .mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - }, - version: "1.2.3", - artifact: { - kind: "legacy-zip", - sha256: "0".repeat(64), - format: "zip", - downloadUrl: "https://clawhub.ai/api/v1/packages/@scope/demo/download?version=1.2.3", - legacyDownloadUrl: - "https://clawhub.ai/api/v1/packages/@scope/demo/download?version=1.2.3", - }, - }); - httpMocks.fetchBinary.mockResolvedValue(bytes); - - await cmdDownloadPackage(makeOpts(workdir), "@scope/demo", { - tag: "latest", - output: "downloads", - }); - - expect(await readFile(join(workdir, "downloads", "scope-demo-1.2.3.zip"))).toEqual( - Buffer.from(bytes), - ); - expect(uiMocks.spinner.fail).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("verifies a local ClawPack against resolved artifact metadata", async () => { - const workdir = await makeTmpWorkdir(); - try { - const bytes = npmPackFixture({ - "package/package.json": JSON.stringify({ - name: "@scope/demo", - version: "1.2.3", - }), - "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), - }); - const identity = artifactIdentity(bytes); - await writeFile(join(workdir, "demo-1.2.3.tgz"), bytes); - httpMocks.apiRequest - .mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - runtimeId: "demo.plugin", - channel: "community", - isOfficial: false, - summary: null, - latestVersion: "1.2.3", - createdAt: 1, - updatedAt: 2, - tags: { latest: "1.2.3" }, - }, - owner: null, - }) - .mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - }, - version: "1.2.3", - artifact: { - kind: "npm-pack", - sha256: identity.sha256, - format: "tgz", - npmIntegrity: identity.npmIntegrity, - npmShasum: identity.npmShasum, - npmTarballName: "demo-1.2.3.tgz", - downloadUrl: "https://clawhub.ai/api/npm/@scope/demo/-/demo-1.2.3.tgz", - }, - }); - - await cmdVerifyPackage(makeOpts(workdir), "demo-1.2.3.tgz", { - packageName: "@scope/demo", - tag: "latest", - }); - - expect(mockLog).toHaveBeenCalledWith("OK. Artifact verification passed."); - expect(uiMocks.spinner.fail).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("fails package artifact verification on digest mismatch", async () => { - const workdir = await makeTmpWorkdir(); - try { - const bytes = npmPackFixture({ - "package/package.json": JSON.stringify({ - name: "@scope/demo", - version: "1.2.3", - }), - }); - await writeFile(join(workdir, "demo-1.2.3.tgz"), bytes); - - await expect( - cmdVerifyPackage(makeOpts(workdir), "demo-1.2.3.tgz", { - sha256: "bad", - }), - ).rejects.toThrow("SHA-256 mismatch"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("sets package release moderation state", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - state: "quarantined", - scanStatus: "malicious", - }); - - await cmdModeratePackageRelease(makeOpts(), "@scope/demo", { - version: "1.2.3", - state: "quarantined", - reason: "suspicious native payload", - }); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/packages/%40scope%2Fdemo/versions/1.2.3/moderation", - token: "tkn", - body: { - state: "quarantined", - reason: "suspicious native payload", - }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith( - "OK. @scope/demo@1.2.3 moderation state set to quarantined.", - ); - }); - - it("reports packages for moderator review", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - reported: true, - alreadyReported: false, - packageId: "pkg_1", - releaseId: "rel_1", - reportCount: 1, - }); - - await cmdReportPackage(makeOpts(), "@scope/demo", { - version: "1.2.3", - reason: "suspicious native payload", - }); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/packages/%40scope%2Fdemo/report", - token: "tkn", - body: { - reason: "suspicious native payload", - version: "1.2.3", - }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("OK. Reported @scope/demo@1.2.3 for moderator review."); - }); - - it("lists package reports", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - items: [ - { - reportId: "packageReports:1", - packageId: "pkg_1", - releaseId: "rel_1", - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - version: "1.2.3", - reason: "suspicious", - status: "open", - createdAt: 1, - reporter: { userId: "users:reporter", handle: "reporter", displayName: "Reporter" }, - triagedAt: null, - triagedBy: null, - triageNote: null, - }, - ], - nextCursor: null, - done: true, - }); - - await cmdListPackageReports(makeOpts(), { status: "open", limit: 10 }); - - const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; - const url = new URL(String(request?.url)); - expect(url.pathname).toBe("/api/v1/packages/reports"); - expect(url.searchParams.get("status")).toBe("open"); - expect(url.searchParams.get("limit")).toBe("10"); - expect(mockLog).toHaveBeenCalledWith("packageReports:1 open @scope/demo@1.2.3"); - }); - - it("triages package reports", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - reportId: "packageReports:1", - packageId: "pkg_1", - status: "confirmed", - reportCount: 0, - actionTaken: "quarantine", - }); - - await cmdTriagePackageReport(makeOpts(), "packageReports:1", { - status: "confirmed", - note: "handled", - action: "quarantine", - yes: true, - }); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/packages/reports/packageReports%3A1/triage", - token: "tkn", - body: { - status: "confirmed", - note: "handled", - finalAction: "quarantine", - }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith( - "OK. Report packageReports:1 set to confirmed; action quarantine.", - ); - expect(mockLog).toHaveBeenCalledWith(" - Quarantine the package release."); - }); - - it("shows package moderation status", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - package: { - packageId: "pkg_1", - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - channel: "community", - isOfficial: false, - reportCount: 2, - lastReportedAt: 456, - scanStatus: "malicious", - }, - latestRelease: { - releaseId: "rel_1", - version: "1.2.3", - artifactKind: "npm-pack", - scanStatus: "malicious", - moderationState: "quarantined", - moderationReason: "manual review", - blockedFromDownload: true, - reasons: ["manual:quarantined", "scan:malicious", "reports:2"], - createdAt: 123, - }, - }); - - await cmdPackageModerationStatus(makeOpts(), "@scope/demo"); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "GET", - path: "/api/v1/packages/%40scope%2Fdemo/moderation", - token: "tkn", - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("@scope/demo moderation"); - expect(mockLog).toHaveBeenCalledWith(" blocked: yes"); - }); - - it("lists the package moderation queue", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - items: [ - { - packageId: "pkg_1", - releaseId: "rel_1", - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - channel: "community", - isOfficial: false, - version: "1.2.3", - createdAt: 1, - artifactKind: "npm-pack", - scanStatus: "malicious", - moderationState: "quarantined", - moderationReason: "manual review", - sourceRepo: "openclaw/demo", - sourceCommit: "abc123", - reportCount: 0, - lastReportedAt: null, - reasons: ["manual:quarantined", "scan:malicious"], - }, - ], - nextCursor: "cursor-1", - done: false, - }); - - await cmdPackageModerationQueue(makeOpts(), { status: "blocked", limit: 10 }); - - const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; - const url = new URL(String(request?.url)); - expect(url.pathname).toBe("/api/v1/packages/moderation/queue"); - expect(url.searchParams.get("status")).toBe("blocked"); - expect(url.searchParams.get("limit")).toBe("10"); - expect(httpMocks.apiRequest.mock.calls[0]?.[1]).toMatchObject({ - method: "GET", - token: "tkn", - }); - expect(mockLog).toHaveBeenCalledWith( - "@scope/demo@1.2.3 malicious quarantined [manual:quarantined, scan:malicious]", - ); - expect(mockLog).toHaveBeenCalledWith("Next cursor: cursor-1"); - }); - - it("dry-runs package artifact metadata backfill by default", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - scanned: 20, - updated: 3, - nextCursor: "cursor-1", - done: false, - dryRun: true, - }); - - await cmdBackfillPackageArtifacts(makeOpts(), { batchSize: 20 }); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/packages/backfill/artifacts", - token: "tkn", - body: { - cursor: null, - batchSize: 20, - dryRun: true, - }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith( - "Dry run package artifact backfill: scanned 20, would update 3.", - ); - expect(mockLog).toHaveBeenCalledWith("Next cursor: cursor-1"); - }); - - it("can apply package artifact backfill across all pages", async () => { - httpMocks.apiRequest - .mockResolvedValueOnce({ - ok: true, - scanned: 100, - updated: 8, - nextCursor: "cursor-2", - done: false, - dryRun: false, - }) - .mockResolvedValueOnce({ - ok: true, - scanned: 5, - updated: 1, - nextCursor: null, - done: true, - dryRun: false, - }); - - await cmdBackfillPackageArtifacts(makeOpts(), { apply: true, all: true, batchSize: 100 }); - - expect(httpMocks.apiRequest).toHaveBeenCalledTimes(2); - expect(httpMocks.apiRequest.mock.calls[0]?.[1]).toMatchObject({ - body: { cursor: null, batchSize: 100, dryRun: false }, - }); - expect(httpMocks.apiRequest.mock.calls[1]?.[1]).toMatchObject({ - body: { cursor: "cursor-2", batchSize: 100, dryRun: false }, - }); - expect(mockLog).toHaveBeenCalledWith( - "Applied package artifact backfill: scanned 105, updated 9.", - ); - }); - - it("prints package readiness checks", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - isOfficial: true, - latestVersion: "1.2.3", - }, - ready: false, - checks: [ - { - id: "clawpack", - label: "ClawPack artifact", - status: "fail", - message: "Latest version is legacy ZIP-only.", - }, - ], - blockers: ["clawpack"], - }); - - await cmdPackageReadiness(makeOpts(), "@scope/demo"); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "GET", - path: "/api/v1/packages/%40scope%2Fdemo/readiness", - token: undefined, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("@scope/demo readiness: blocked"); - expect(mockLog).toHaveBeenCalledWith("FAIL clawpack: Latest version is legacy ZIP-only."); - expect(mockLog).toHaveBeenCalledWith("Blockers: clawpack"); - }); - - it("prints package migration status checks", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - package: { - name: "@scope/demo", - displayName: "Demo", - family: "code-plugin", - isOfficial: true, - latestVersion: "1.2.3", - }, - ready: true, - checks: [ - { - id: "clawpack", - label: "ClawPack artifact", - status: "pass", - message: "Latest version has a ClawPack artifact.", - }, - ], - blockers: [], - }); - - await cmdPackageMigrationStatus(makeOpts(), "@scope/demo"); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "GET", - path: "/api/v1/packages/%40scope%2Fdemo/readiness", - token: undefined, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("@scope/demo migration: ready"); - expect(mockLog).toHaveBeenCalledWith("Version: 1.2.3"); - expect(mockLog).toHaveBeenCalledWith("Official: yes"); - expect(mockLog).toHaveBeenCalledWith("PASS clawpack: Latest version has a ClawPack artifact."); - }); - - it("lists package migration rows", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - items: [ - { - migrationId: "officialPluginMigrations:1", - bundledPluginId: "core.search", - packageName: "@scope/demo", - packageId: "pkg_1", - owner: "platform", - sourceRepo: "openclaw/openclaw", - sourcePath: "plugins/search", - sourceCommit: "abc123", - phase: "blocked", - blockers: ["missing ClawPack"], - hostTargetsComplete: true, - scanClean: false, - moderationApproved: false, - runtimeBundlesReady: false, - notes: "needs publisher upload", - createdAt: 100, - updatedAt: 200, - }, - ], - nextCursor: null, - done: true, - }); - - await cmdListPackageMigrations(makeOpts(), { phase: "blocked", limit: 10 }); - - const url = new URL(httpMocks.apiRequest.mock.calls[0]?.[1].url as string); - expect(url.pathname).toBe("/api/v1/packages/migrations"); - expect(url.searchParams.get("phase")).toBe("blocked"); - expect(url.searchParams.get("limit")).toBe("10"); - expect(mockLog).toHaveBeenCalledWith("core.search blocked @scope/demo blockers:1"); - expect(mockLog).toHaveBeenCalledWith(" source: openclaw/openclaw plugins/search abc123"); - expect(mockLog).toHaveBeenCalledWith(" notes: needs publisher upload"); - }); - - it("upserts package migration rows", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - migration: { - migrationId: "officialPluginMigrations:1", - bundledPluginId: "core.search", - packageName: "@scope/demo", - packageId: "pkg_1", - owner: "platform", - sourceRepo: "openclaw/openclaw", - sourcePath: "plugins/search", - sourceCommit: null, - phase: "blocked", - blockers: ["missing ClawPack"], - hostTargetsComplete: true, - scanClean: false, - moderationApproved: false, - runtimeBundlesReady: false, - notes: null, - createdAt: 100, - updatedAt: 200, - }, - }); - - await cmdUpsertPackageMigration(makeOpts(), "core.search", { - package: "@scope/demo", - owner: "platform", - sourceRepo: "openclaw/openclaw", - sourcePath: "plugins/search", - phase: "blocked", - blockers: "missing ClawPack", - hostTargetsComplete: true, - }); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/packages/migrations", - token: "tkn", - body: { - bundledPluginId: "core.search", - packageName: "@scope/demo", - owner: "platform", - sourceRepo: "openclaw/openclaw", - sourcePath: "plugins/search", - phase: "blocked", - blockers: ["missing ClawPack"], - hostTargetsComplete: true, - }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("OK. Migration core.search is blocked for @scope/demo."); - }); - - it("publishes a code plugin package with an exact explicit payload", async () => { - const workdir = await makeTmpWorkdir(); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(123_456_789); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - files: ["dist", "openclaw.plugin.json"], - }), - "utf8", - ); - await writeFile(join(folder, ".gitignore"), "dist/\n", "utf8"); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - owner: "@openclaw", - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - sourceRef: "refs/tags/v1.0.0", - clawscanNote: "This plugin shells out only to the bundled helper binary.", - }); - - expect(getPublishPayload()).toEqual({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - ownerHandle: "openclaw", - family: "code-plugin", - version: "1.0.0", - changelog: "", - clawScanNote: "This plugin shells out only to the bundled helper binary.", - tags: ["latest"], - source: { - kind: "github", - url: "https://github.com/openclaw/demo-plugin", - repo: "openclaw/demo-plugin", - ref: "refs/tags/v1.0.0", - commit: "abc123", - path: ".", - importedAt: 123_456_789, - }, - }); - expect(getUploadedFileNames()).toEqual([]); - expect(getUploadedClawPackNames()).toEqual(["scope-demo-plugin-1.0.0.tgz"]); - expect(httpMocks.apiRequestForm.mock.calls[0]?.[1]).toEqual( - expect.objectContaining({ retryCount: 5 }), - ); - const uploadedPack = getUploadedClawPacks()[0]; - if (!uploadedPack) throw new Error("Missing uploaded ClawPack"); - const parsed = parseClawPack(new Uint8Array(await uploadedPack.arrayBuffer())); - expect(parsed.entries.map((entry) => entry.path).sort()).toEqual([ - "dist/index.js", - "openclaw.plugin.json", - "package.json", - ]); - expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( - "OK. Published @scope/demo-plugin@1.0.0 (rel_1)", - ); - expect(uiMocks.spinner.fail).not.toHaveBeenCalled(); - expect(mockLog).not.toHaveBeenCalled(); - expect(mockWrite).not.toHaveBeenCalled(); - dateSpy.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("resolves package publish dot paths from the caller cwd before the OpenClaw workdir", async () => { - const workspace = await makeTmpWorkdir(); - const pluginRoot = await makeTmpWorkdir(); - const previousCwd = process.cwd(); - try { - await mkdir(join(pluginRoot, "dist"), { recursive: true }); - await writeFile( - join(pluginRoot, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/cwd-plugin", - displayName: "Cwd Plugin", - version: "1.0.0", - files: ["dist", "openclaw.plugin.json"], - }), - "utf8", - ); - await writeFile( - join(pluginRoot, "openclaw.plugin.json"), - JSON.stringify({ id: "cwd.plugin", configSchema: { type: "object" } }), - "utf8", - ); - await writeFile(join(pluginRoot, "dist", "index.js"), "export const demo = true;\n", "utf8"); - - process.chdir(pluginRoot); - - await cmdPublishPackage(makeOpts(workspace), ".", { - dryRun: true, - sourceRepo: "openclaw/cwd-plugin", - sourceCommit: "abc123", - }); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toContain("Name: @scope/cwd-plugin"); - expect(output).toContain("Files: 3"); - } finally { - process.chdir(previousCwd); - await rm(pluginRoot, { recursive: true, force: true }); - await rm(workspace, { recursive: true, force: true }); - } - }); - - it("rejects oversized clawscan notes before uploading package files", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - await expect( - cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - clawscanNote: "x".repeat(MAX_CLAWSCAN_NOTE_CHARS + 1), - }), - ).rejects.toThrow(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("publishes a ClawPack tarball without uploading extracted files", async () => { - const workdir = await makeTmpWorkdir(); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(123_456_789); - try { - const packName = "demo-plugin-1.0.0.tgz"; - await writeFile( - join(workdir, packName), - npmPackFixture({ - "package/package.json": makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), - "package/dist/index.js": "export const demo = true;\n", - }), - ); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), packName, { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - expect(getPublishPayload()).toEqual({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - family: "code-plugin", - version: "1.0.0", - changelog: "", - tags: ["latest"], - source: { - kind: "github", - url: "https://github.com/openclaw/demo-plugin", - repo: "openclaw/demo-plugin", - ref: "abc123", - commit: "abc123", - path: ".", - importedAt: 123_456_789, - }, - }); - expect(getUploadedClawPackNames()).toEqual([packName]); - expect(getUploadedFileNames()).toEqual([]); - expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( - "OK. Published @scope/demo-plugin@1.0.0 (rel_1)", - ); - dateSpy.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("packs a plugin folder through npm pack and validates the ClawPack", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await mkdir(join(workdir, "packs"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - description: "Demo plugin", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); - - await cmdPackPackage(makeOpts(workdir), "demo-plugin", { - packDestination: "packs", - }); - - const packPath = join(workdir, "packs", "scope-demo-plugin-1.0.0.tgz"); - const parsed = parseClawPack(new Uint8Array(await readFile(packPath))); - expect(parsed.packageName).toBe("@scope/demo-plugin"); - expect(parsed.packageVersion).toBe("1.0.0"); - expect(parsed.entries.map((entry) => entry.path)).toContain("openclaw.plugin.json"); - expect(mockLog).toHaveBeenCalledWith(`Path: ${packPath}`); - expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( - `Packed @scope/demo-plugin@1.0.0 -> ${packPath}`, - ); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects a code plugin ClawPack with TypeScript entries and no compiled runtime", async () => { - const workdir = await makeTmpWorkdir(); - try { - const packName = "demo-plugin-1.0.0.tgz"; - await writeFile( - join(workdir, packName), - npmPackFixture({ - "package/package.json": makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - openclaw: { - extensions: ["./index.ts"], - compat: { - pluginApi: ">=2026.3.24-beta.2", - }, - build: { - openclawVersion: "2026.3.24-beta.2", - }, - }, - }), - "package/openclaw.plugin.json": JSON.stringify({ id: "demo.plugin" }), - "package/index.ts": "export const demo = true;\n", - }), - ); - - await expect( - cmdPublishPackage(makeOpts(workdir), packName, { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }), - ).rejects.toThrow( - "@scope/demo-plugin requires compiled runtime output for TypeScript entry ./index.ts", - ); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects a ClawPack tarball without openclaw.plugin.json", async () => { - const workdir = await makeTmpWorkdir(); - try { - const packName = "demo-plugin-1.0.0.tgz"; - await writeFile( - join(workdir, packName), - npmPackFixture({ - "package/package.json": makeCodePluginPackageJson({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "package/dist/index.js": "export const demo = true;\n", - }), - ); - - await expect( - cmdPublishPackage(makeOpts(workdir), packName, { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }), - ).rejects.toThrow("ClawPack must contain package/openclaw.plugin.json"); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("mints a short-lived publish token from GitHub Actions OIDC in CI", async () => { - const workdir = await makeTmpWorkdir(); - try { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ value: "github-oidc-jwt" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - vi.stubGlobal("fetch", fetchMock); - - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - httpMocks.apiRequest.mockResolvedValueOnce({ - token: "clh_short_publish", - expiresAt: 1_234_567_890, - }); - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); - expect(fetchMock).toHaveBeenCalledWith( - new URL("https://token.actions.githubusercontent.com/oidc?audience=clawhub"), - expect.objectContaining({ - method: "GET", - headers: expect.objectContaining({ - Authorization: "Bearer gh-request-token", - }), - }), - ); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "POST", - path: "/api/v1/publish/token/mint", - body: { - packageName: "@scope/demo-plugin", - version: "1.0.0", - githubOidcToken: "github-oidc-jwt", - }, - }), - expect.anything(), - ); - const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as - | { token?: string } - | undefined; - expect(publishArgs?.token).toBe("clh_short_publish"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("uses normal token auth for manual override publishes", async () => { - const workdir = await makeTmpWorkdir(); - try { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; - const fetchMock = vi.fn(); - vi.stubGlobal("fetch", fetchMock); - - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - authTokenMocks.requireAuthToken.mockResolvedValueOnce("manual-token"); - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - manualOverrideReason: "break glass", - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - expect(fetchMock).not.toHaveBeenCalled(); - expect(httpMocks.apiRequest).not.toHaveBeenCalled(); - const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as - | { token?: string; form?: FormData } - | undefined; - expect(publishArgs?.token).toBe("manual-token"); - const payloadEntry = publishArgs?.form?.get("payload"); - if (typeof payloadEntry !== "string") { - throw new Error("Missing publish payload"); - } - expect(JSON.parse(payloadEntry)).toMatchObject({ - manualOverrideReason: "break glass", - }); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("falls back to a normal auth token when trusted minting is unavailable", async () => { - const workdir = await makeTmpWorkdir(); - try { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ value: "github-oidc-jwt" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - vi.stubGlobal("fetch", fetchMock); - - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - authTokenMocks.requireAuthToken.mockResolvedValueOnce("fallback-token"); - httpMocks.apiRequest.mockRejectedValueOnce( - Object.assign(new Error("Trusted publisher config is not set"), { status: 403 }), - ); - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as - | { token?: string } - | undefined; - expect(publishArgs?.token).toBe("fallback-token"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("falls back to a normal auth token when trusted minting returns a 400", async () => { - const workdir = await makeTmpWorkdir(); - try { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; - const fetchMock = vi.fn().mockResolvedValue( - new Response(JSON.stringify({ value: "github-oidc-jwt" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - vi.stubGlobal("fetch", fetchMock); - - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - authTokenMocks.requireAuthToken.mockResolvedValueOnce("fallback-token"); - httpMocks.apiRequest.mockRejectedValueOnce( - Object.assign(new Error("Trusted publishing requires workflow_dispatch"), { status: 400 }), - ); - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as - | { token?: string } - | undefined; - expect(publishArgs?.token).toBe("fallback-token"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("falls back to a normal auth token when requesting the GitHub OIDC token fails", async () => { - const workdir = await makeTmpWorkdir(); - try { - process.env.ACTIONS_ID_TOKEN_REQUEST_URL = "https://token.actions.githubusercontent.com/oidc"; - process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "gh-request-token"; - const fetchMock = vi.fn().mockResolvedValue( - new Response("oidc unavailable", { - status: 500, - statusText: "Internal Server Error", - }), - ); - vi.stubGlobal("fetch", fetchMock); - - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - authTokenMocks.requireAuthToken.mockResolvedValueOnce("fallback-token"); - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - const publishArgs = httpMocks.apiRequestForm.mock.calls[0]?.[1] as - | { token?: string } - | undefined; - expect(publishArgs?.token).toBe("fallback-token"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("publishes a bundle plugin package with real bundle marker detection", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-bundle"); - await mkdir(join(folder, "dist"), { recursive: true }); - await mkdir(join(folder, ".codex-plugin"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - JSON.stringify({ - name: "demo-bundle", - displayName: "Demo Bundle", - version: "0.4.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.bundle" }), - "utf8", - ); - await writeFile( - join(folder, ".codex-plugin", "plugin.json"), - JSON.stringify({ name: "Demo Bundle", skills: ["skills"] }), - "utf8", - ); - await writeFile(join(folder, "dist", "plugin.wasm"), "binary", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_bundle", - releaseId: "rel_bundle", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-bundle", { - bundleFormat: "openclaw-bundle", - hostTargets: "desktop,mobile", - }); - - expect(getPublishPayload()).toEqual({ - name: "demo-bundle", - displayName: "Demo Bundle", - family: "bundle-plugin", - version: "0.4.0", - changelog: "", - tags: ["latest"], - bundle: { - format: "openclaw-bundle", - hostTargets: ["desktop", "mobile"], - }, - }); - expect(getUploadedFileNames()).toEqual([ - ".codex-plugin/plugin.json", - "dist/plugin.wasm", - "openclaw.plugin.json", - "package.json", - ]); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects code-plugin publish without source metadata", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ name: "demo-plugin", version: "1.0.0" }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - await expect(cmdPublishPackage(makeOpts(workdir), "demo-plugin", {})).rejects.toThrow( - "--source-repo and --source-commit required for code plugins", - ); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects code-plugin publish when openclaw.plugin.json is missing", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ name: "demo-plugin", displayName: "Demo", version: "1.0.0" }), - "utf8", - ); - - await expect( - cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - family: "code-plugin", - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }), - ).rejects.toThrow("openclaw.plugin.json required"); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects code-plugin publish when required OpenClaw compatibility metadata is missing", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - JSON.stringify({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - openclaw: { - extensions: ["./index.ts"], - }, - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin", configSchema: { type: "object" } }), - "utf8", - ); - - await expect( - cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }), - ).rejects.toThrow( - "openclaw.compat.pluginApi is required for external code plugins published to ClawHub.", - ); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("publishes code plugins when host targets are missing", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - JSON.stringify({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - openclaw: { - extensions: ["./index.ts"], - compat: { pluginApi: ">=2026.3.24-beta.2" }, - build: { openclawVersion: "2026.3.24-beta.2" }, - environment: {}, - }, - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin", configSchema: { type: "object" } }), - "utf8", - ); - await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: "abc123", - }); - - expect(getPublishPayload()).toMatchObject({ - name: "demo-plugin", - family: "code-plugin", - version: "1.0.0", - }); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects bundle-plugin publish when openclaw.plugin.json is missing", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-bundle"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - JSON.stringify({ name: "demo-bundle", displayName: "Demo Bundle", version: "0.1.0" }), - "utf8", - ); - - await expect( - cmdPublishPackage(makeOpts(workdir), "demo-bundle", { family: "bundle-plugin" }), - ).rejects.toThrow("openclaw.plugin.json required"); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("respects package ignore rules and built-in ignored directories", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "ignored-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await mkdir(join(folder, "node_modules", "pkg"), { recursive: true }); - await mkdir(join(folder, ".git"), { recursive: true }); - await mkdir(join(folder, ".codex-plugin"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - JSON.stringify({ - name: "ignored-plugin", - displayName: "Ignored Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "ignored.plugin" }), - "utf8", - ); - await writeFile( - join(folder, ".codex-plugin", "plugin.json"), - JSON.stringify({ name: "Ignored Plugin", skills: ["skills"] }), - "utf8", - ); - await writeFile(join(folder, ".clawhubignore"), "ignored.txt\n", "utf8"); - await writeFile(join(folder, "dist", "index.js"), "export {};\n", "utf8"); - await writeFile(join(folder, "ignored.txt"), "ignore me\n", "utf8"); - await writeFile( - join(folder, "node_modules", "pkg", "index.js"), - "module.exports = {};\n", - "utf8", - ); - await writeFile(join(folder, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_ignored", - releaseId: "rel_ignored", - }); - - await cmdPublishPackage(makeOpts(workdir), "ignored-plugin", { - sourceRepo: "openclaw/ignored-plugin", - sourceCommit: "abc123", - }); - - expect(getUploadedFileNames()).toEqual([ - ".clawhubignore", - ".codex-plugin/plugin.json", - "dist/index.js", - "openclaw.plugin.json", - "package.json", - ]); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("reports publish failures through the spinner without writing to stdout", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "broken-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "broken-plugin", - displayName: "Broken Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "broken.plugin" }), - "utf8", - ); - - httpMocks.apiRequestForm.mockRejectedValueOnce(new Error("Registry rejected upload")); - - await expect( - cmdPublishPackage(makeOpts(workdir), "broken-plugin", { - sourceRepo: "openclaw/broken-plugin", - sourceCommit: "deadbeef", - }), - ).rejects.toThrow("Registry rejected upload"); - - expect(uiMocks.spinner.fail).toHaveBeenCalledWith("Registry rejected upload"); - expect(uiMocks.spinner.succeed).not.toHaveBeenCalled(); - expect(mockLog).not.toHaveBeenCalled(); - expect(mockWrite).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("auto-detects local git source metadata and matches the explicit payload", async () => { - const workdir = await makeTmpWorkdir(); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(987_654_321); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); - - runGit(folder, ["init", "-b", "main"]); - runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); - runGit(folder, ["add", "."]); - runGit(folder, [ - "-c", - "user.name=Test", - "-c", - "user.email=test@example.com", - "commit", - "-m", - "init", - ]); - const commit = runGit(folder, ["rev-parse", "HEAD"]); - runGit(folder, ["-c", "tag.gpgSign=false", "tag", "v1.0.0"]); - - httpMocks.apiRequestForm.mockResolvedValue({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/demo-plugin", - sourceCommit: commit, - sourceRef: "v1.0.0", - }); - const explicitPayload = getPublishPayload(); - const explicitFiles = getUploadedFileNames(); - - httpMocks.apiRequestForm.mockClear(); - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", {}); - const inferredPayload = getPublishPayload(); - const inferredFiles = getUploadedFileNames(); - - expect(inferredPayload).toEqual(explicitPayload); - expect(inferredFiles).toEqual(explicitFiles); - dateSpy.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("lets explicit source flags override inferred git metadata", async () => { - const workdir = await makeTmpWorkdir(); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(222_222_222); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - runGit(folder, ["init", "-b", "main"]); - runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); - runGit(folder, ["add", "."]); - runGit(folder, [ - "-c", - "user.name=Test", - "-c", - "user.email=test@example.com", - "commit", - "-m", - "init", - ]); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { - sourceRepo: "openclaw/override-plugin", - sourceCommit: "feedface", - sourceRef: "refs/heads/release", - sourcePath: "custom/path", - }); - - expect(getPublishPayload()).toEqual({ - name: "demo-plugin", - displayName: "Demo Plugin", - family: "code-plugin", - version: "1.0.0", - changelog: "", - tags: ["latest"], - source: { - kind: "github", - url: "https://github.com/openclaw/override-plugin", - repo: "openclaw/override-plugin", - ref: "refs/heads/release", - commit: "feedface", - path: "custom/path", - importedAt: 222_222_222, - }, - }); - dateSpy.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("preserves inferred source subpaths for nested local plugin folders", async () => { - const workdir = await makeTmpWorkdir(); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(333_333_333); - try { - const folder = join(workdir, "packages", "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin", configSchema: { type: "object" } }), - "utf8", - ); - - runGit(workdir, ["init", "-b", "main"]); - runGit(workdir, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); - runGit(workdir, ["add", "."]); - runGit(workdir, [ - "-c", - "user.name=Test", - "-c", - "user.email=test@example.com", - "commit", - "-m", - "init", - ]); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "packages/demo-plugin", {}); - - expect(getPublishPayload()).toEqual({ - name: "demo-plugin", - displayName: "Demo Plugin", - family: "code-plugin", - version: "1.0.0", - changelog: "", - tags: ["latest"], - source: { - kind: "github", - url: "https://github.com/openclaw/demo-plugin", - repo: "openclaw/demo-plugin", - ref: "main", - commit: expect.any(String), - path: "packages/demo-plugin", - importedAt: 333_333_333, - }, - }); - dateSpy.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("uses --source-path as the package folder for GitHub shorthand sources", async () => { - const workdir = await makeTmpWorkdir(); - const originalFetch = globalThis.fetch; - const commit = "0123456789abcdef0123456789abcdef01234567"; - const archiveBytes = zipSync({ - "repo-root/plugins/demo/package.json": new TextEncoder().encode( - makeCodePluginPackageJson({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - ), - "repo-root/plugins/demo/openclaw.plugin.json": new TextEncoder().encode( - JSON.stringify({ id: "demo.plugin" }), - ), - "repo-root/plugins/demo/dist/index.js": new TextEncoder().encode("export {};\n"), - "repo-root/other/package.json": new TextEncoder().encode('{"name":"wrong"}\n'), - }); - const archiveBody = archiveBytes.buffer.slice( - archiveBytes.byteOffset, - archiveBytes.byteOffset + archiveBytes.byteLength, - ) as ArrayBuffer; - const fetchMock = vi.fn(async (input) => { - const url = input instanceof Request ? input.url : input.toString(); - if (url.endsWith("/repos/owner/repo/commits/main")) { - return new Response(JSON.stringify({ sha: commit }), { - status: 200, - headers: { "content-type": "application/json" }, - }); - } - if (url.endsWith(`/repos/owner/repo/zipball/${commit}`)) { - return new Response(archiveBody, { - status: 200, - headers: { "content-type": "application/zip" }, - }); - } - throw new Error(`Unexpected fetch: ${url}`); - }); - - Object.defineProperty(globalThis, "fetch", { - value: fetchMock, - configurable: true, - writable: true, - }); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(555_555_555); - - try { - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - packageId: "pkg_1", - releaseId: "rel_1", - }); - - await cmdPublishPackage(makeOpts(workdir), "owner/repo@main", { - sourcePath: "plugins/demo", - }); - - expect(getUploadedFileNames()).toEqual([]); - expect(getUploadedClawPackNames()).toEqual(["scope-demo-plugin-1.0.0.tgz"]); - expect(getPublishPayload()).toEqual({ - name: "@scope/demo-plugin", - displayName: "Demo Plugin", - family: "code-plugin", - version: "1.0.0", - changelog: "", - tags: ["latest"], - source: { - kind: "github", - url: "https://github.com/owner/repo", - repo: "owner/repo", - ref: "main", - commit, - path: "plugins/demo", - importedAt: 555_555_555, - }, - }); - } finally { - Object.defineProperty(globalThis, "fetch", { - value: originalFetch, - configurable: true, - writable: true, - }); - dateSpy.mockRestore(); - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("supports dry-run without auth or publish and prints a summary", async () => { - const workdir = await makeTmpWorkdir(); - const dateSpy = vi.spyOn(Date, "now").mockReturnValue(444_444_444); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(join(folder, "dist"), { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - await writeFile(join(folder, "dist", "index.js"), "export const demo = true;\n", "utf8"); - - runGit(folder, ["init", "-b", "main"]); - runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); - runGit(folder, ["add", "."]); - runGit(folder, [ - "-c", - "user.name=Test", - "-c", - "user.email=test@example.com", - "commit", - "-m", - "init", - ]); - const commit = runGit(folder, ["rev-parse", "HEAD"]); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { dryRun: true }); - - expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - expect(mockLog.mock.calls.map((call) => call[0])).toEqual( - expect.arrayContaining([ - "Dry run - nothing will be published.", - expect.stringMatching(/Source:\s+github:openclaw\/demo-plugin@main/), - expect.stringMatching(/Name:\s+demo-plugin/), - expect.stringMatching(new RegExp(`Commit:\\s+${commit}`)), - "Files:", - ]), - ); - expect(mockWrite).not.toHaveBeenCalled(); - dateSpy.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("supports dry-run json output without auth or publish", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - makeCodePluginPackageJson({ - name: "demo-plugin", - displayName: "Demo Plugin", - version: "1.0.0", - }), - "utf8", - ); - await writeFile( - join(folder, "openclaw.plugin.json"), - JSON.stringify({ id: "demo.plugin" }), - "utf8", - ); - - runGit(folder, ["init", "-b", "main"]); - runGit(folder, ["remote", "add", "origin", "git@github.com:openclaw/demo-plugin.git"]); - runGit(folder, ["add", "."]); - runGit(folder, [ - "-c", - "user.name=Test", - "-c", - "user.email=test@example.com", - "commit", - "-m", - "init", - ]); - - await cmdPublishPackage(makeOpts(workdir), "demo-plugin", { dryRun: true, json: true }); - - expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - expect(mockLog).not.toHaveBeenCalled(); - expect(mockWrite).toHaveBeenCalledTimes(1); - const output = String(mockWrite.mock.calls[0]?.[0] ?? "").trim(); - expect(JSON.parse(output)).toEqual({ - source: "github:openclaw/demo-plugin@main", - name: "demo-plugin", - displayName: "Demo Plugin", - family: "code-plugin", - version: "1.0.0", - commit: expect.any(String), - files: 2, - totalBytes: expect.any(Number), - }); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("gets trusted publisher config for a package", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - trustedPublisher: { - provider: "github-actions", - repository: "openclaw/openclaw", - repositoryId: "1", - repositoryOwner: "openclaw", - repositoryOwnerId: "2", - workflowFilename: "plugin-clawhub-release.yml", - environment: "clawhub-release", - }, - }); - - await cmdGetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo"); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "GET", - path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", - }), - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("Provider: github-actions"); - expect(mockLog).toHaveBeenCalledWith("Repository: openclaw/openclaw"); - expect(mockLog).toHaveBeenCalledWith("Workflow: plugin-clawhub-release.yml"); - expect(mockLog).toHaveBeenCalledWith("Environment: clawhub-release"); - }); - - it("gets trusted publisher config without a pinned environment", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - trustedPublisher: { - provider: "github-actions", - repository: "openclaw/openclaw", - repositoryId: "1", - repositoryOwner: "openclaw", - repositoryOwnerId: "2", - workflowFilename: "plugin-clawhub-release.yml", - }, - }); - - await cmdGetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo"); - - expect(mockLog).toHaveBeenCalledWith("Provider: github-actions"); - expect(mockLog).toHaveBeenCalledWith("Repository: openclaw/openclaw"); - expect(mockLog).toHaveBeenCalledWith("Workflow: plugin-clawhub-release.yml"); - expect(mockLog).not.toHaveBeenCalledWith(expect.stringContaining("Environment:")); - }); - - it("sets trusted publisher config for a package", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - trustedPublisher: { - provider: "github-actions", - repository: "openclaw/openclaw", - repositoryId: "1", - repositoryOwner: "openclaw", - repositoryOwnerId: "2", - workflowFilename: "plugin-clawhub-release.yml", - environment: "clawhub-release", - }, - }); - - await cmdSetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo", { - repository: "openclaw/openclaw", - workflowFilename: "plugin-clawhub-release.yml", - environment: "clawhub-release", - }); - - expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "POST", - path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", - token: "tkn", - body: { - repository: "openclaw/openclaw", - workflowFilename: "plugin-clawhub-release.yml", - environment: "clawhub-release", - }, - }), - expect.anything(), - ); - }); - - it("sets trusted publisher config for a package without environment", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - trustedPublisher: { - provider: "github-actions", - repository: "openclaw/openclaw", - repositoryId: "1", - repositoryOwner: "openclaw", - repositoryOwnerId: "2", - workflowFilename: "plugin-clawhub-release.yml", - }, - }); - - await cmdSetPackageTrustedPublisher(makeOpts(), "@openclaw/zalo", { - repository: "openclaw/openclaw", - workflowFilename: "plugin-clawhub-release.yml", - }); - - expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "POST", - path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", - token: "tkn", - body: { - repository: "openclaw/openclaw", - workflowFilename: "plugin-clawhub-release.yml", - }, - }), - expect.anything(), - ); - }); - - it("deletes trusted publisher config for a package", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - - await cmdDeletePackageTrustedPublisher(makeOpts(), "@openclaw/zalo"); - - expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "DELETE", - path: "/api/v1/packages/%40openclaw%2Fzalo/trusted-publisher", - token: "tkn", - }), - undefined, - ); - }); - - it("soft-deletes a package with confirmation bypass", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - - await cmdDeletePackage(makeOpts(), "@openclaw/zalo", { yes: true }, false); - - expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "DELETE", - path: "/api/v1/packages/%40openclaw%2Fzalo", - token: "tkn", - }), - expect.anything(), - ); - }); - - it("transfers a package to another publisher", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - packageId: "packages:opik", - name: "@opik/opik-openclaw", - ownerUserId: "users:vincent", - ownerPublisherId: "publishers:opik", - channel: "community", - isOfficial: false, - }); - - await cmdTransferPackage(makeOpts(), "@opik/opik-openclaw", { to: "opik" }); - - expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "POST", - path: "/api/v1/packages/%40opik%2Fopik-openclaw/transfer", - token: "tkn", - body: { toOwner: "opik" }, - }), - expect.anything(), - ); - }); - - it("requires --yes for non-interactive package deletes", async () => { - await expect(cmdDeletePackage(makeOpts(), "@openclaw/zalo", {}, false)).rejects.toThrow( - /--yes/i, - ); - expect(httpMocks.apiRequest).not.toHaveBeenCalled(); - }); - - it("restores package deletes through the undelete endpoint", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - - await cmdUndeletePackage(makeOpts(), "@openclaw/zalo", { yes: true }, false); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "POST", - path: "/api/v1/packages/%40openclaw%2Fzalo/undelete", - token: "tkn", - }), - expect.anything(), - ); - }); -}); diff --git a/dt-skill/src/cli/commands/packages.ts b/dt-skill/src/cli/commands/packages.ts index 53a9a944..995362b5 100644 --- a/dt-skill/src/cli/commands/packages.ts +++ b/dt-skill/src/cli/commands/packages.ts @@ -1,58 +1,33 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { basename, dirname, join, relative, resolve, sep } from "node:path"; -import ignore from "ignore"; -import mime from "mime"; import semver from "semver"; import { parseClawPack } from "../../clawpack.js"; -import { apiRequest, apiRequestForm, fetchBinary, fetchText, registryUrl } from "../../http.js"; +import { apiRequest, fetchBinary, fetchText, registryUrl } from "../../http.js"; import { ApiRoutes, - ApiV1DeleteResponseSchema, ApiV1PackageArtifactResponseSchema, ApiV1PackageListResponseSchema, - ApiV1PackageModerationStatusResponseSchema, - ApiV1PackagePublishResponseSchema, ApiV1PackageReadinessResponseSchema, - ApiV1PackageReportResponseSchema, ApiV1PackageResponseSchema, ApiV1PackageSearchResponseSchema, - ApiV1PackageTransferResponseSchema, - ApiV1PackageTrustedPublisherResponseSchema, ApiV1PackageVersionListResponseSchema, ApiV1PackageVersionResponseSchema, - ApiV1PublishTokenMintResponseSchema, - normalizeClawScanNote, - normalizeOpenClawExternalPluginCompatibility, type PackageArtifactSummary, type PackageCapabilitySummary, type PackageCompatibility, type PackageFamily, - type PackageTrustedPublisher, type PackageVerificationSummary, validateOpenClawExternalCodePluginPackageContents, validateOpenClawExternalCodePluginPackageJson, } from "../../schema/index.js"; -import { getOptionalAuthToken, requireAuthToken } from "../authToken.js"; import { getRegistry } from "../registry.js"; -import { titleCase } from "../slug.js"; import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; -import { - fetchGitHubSource, - normalizeGitHubRepo, - resolveLocalGitInfo, - resolveSourceInput, -} from "./github.js"; - -const DOT_DIR = ".clawhub"; -const LEGACY_DOT_DIR = ".clawdhub"; -const DOT_IGNORE = ".clawhubignore"; -const LEGACY_DOT_IGNORE = ".clawdhubignore"; +import { createSpinner, fail, formatError } from "../ui.js"; +import { resolveSourceInput } from "./github.js"; + const MAX_CLAWPACK_BYTES = 120 * 1024 * 1024; -const PACKAGE_PUBLISH_RETRY_COUNT = 5; type PackageInspectOptions = { version?: string; @@ -85,26 +60,6 @@ type PackageExploreOptions = { json?: boolean; }; -type PackagePublishOptions = { - family?: "code-plugin" | "bundle-plugin"; - name?: string; - displayName?: string; - owner?: string; - version?: string; - changelog?: string; - clawscanNote?: string; - manualOverrideReason?: string; - tags?: string; - bundleFormat?: string; - hostTargets?: string; - sourceRepo?: string; - sourceCommit?: string; - sourceRef?: string; - sourcePath?: string; - dryRun?: boolean; - json?: boolean; -}; - type PackagePackOptions = { packDestination?: string; json?: boolean; @@ -128,93 +83,18 @@ type PackageVerifyOptions = { json?: boolean; }; -type PackageReportOptions = { - version?: string; - reason?: string; - json?: boolean; -}; - -type PackageModerationStatusOptions = { - json?: boolean; -}; - type PackageReadinessOptions = { json?: boolean; }; type PackageMigrationStatusOptions = PackageReadinessOptions; -type PackageTrustedPublisherGetOptions = { - json?: boolean; -}; - -type PackageDeleteOptions = { - yes?: boolean; - json?: boolean; -}; - -type PackageUndeleteOptions = PackageDeleteOptions; - -type PackageTransferOptions = { - to: string; - reason?: string; - json?: boolean; -}; - type PackageFile = { relPath: string; bytes: Uint8Array; contentType?: string; }; -type InferredPublishSource = { - repo?: string; - commit?: string; - ref?: string; - path?: string; - url?: string; -}; - -type PackagePublishSource = ReturnType; - -type PackagePublishPayload = { - name: string; - displayName: string; - ownerHandle?: string; - family: "code-plugin" | "bundle-plugin"; - version: string; - changelog: string; - clawScanNote?: string; - manualOverrideReason?: string; - tags: string[]; - source?: NonNullable; - bundle?: { - format?: string; - hostTargets: string[]; - }; -}; - -type PackagePublishPlan = { - folder: string; - cleanup?: () => Promise; - filesOnDisk: PackageFile[]; - clawpackOnDisk?: PackageFile; - packageJson?: unknown; - payload: PackagePublishPayload; - compatibility?: PackageCompatibility; - sourceLabel: string; - output: { - source: string; - name: string; - displayName: string; - family: "code-plugin" | "bundle-plugin"; - version: string; - commit?: string; - files: number; - totalBytes: number; - }; -}; - type PackedClawPack = { path: string; file: PackageFile; @@ -261,7 +141,6 @@ export async function cmdExplorePackages( options: PackageExploreOptions = {}, ) { const trimmedQuery = query.trim(); - const token = await getOptionalAuthToken(); const registry = await getRegistry(opts, { cache: true }); const spinner = createSpinner(trimmedQuery ? "Searching packages" : "Listing packages"); try { @@ -278,7 +157,7 @@ export async function cmdExplorePackages( appendPackageExploreFilters(url, options); const result = await apiRequest( registry, - { method: "GET", url: url.toString(), token }, + { method: "GET", url: url.toString() }, ApiV1PackageSearchResponseSchema, ); spinner.stop(); @@ -312,7 +191,7 @@ export async function cmdExplorePackages( appendPackageExploreFilters(url, options); const result = await apiRequest( registry, - { method: "GET", url: url.toString(), token }, + { method: "GET", url: url.toString() }, ApiV1PackageListResponseSchema, ); spinner.stop(); @@ -340,12 +219,10 @@ export async function cmdInspectPackage( ) { const trimmed = normalizePackageNameOrFail(packageName); if (options.version && options.tag) fail("Use either --version or --tag"); - - const token = await getOptionalAuthToken(); const registry = await getRegistry(opts, { cache: true }); const spinner = createSpinner("Fetching package"); try { - const detail = await apiRequestPackageDetail(registry, trimmed, token); + const detail = await apiRequestPackageDetail(registry, trimmed); if (!detail.package) { spinner.fail("Package not found"); return; @@ -365,14 +242,14 @@ export async function cmdInspectPackage( const targetVersion = requestedVersion ?? latestVersion; if (!targetVersion) fail("Could not resolve latest version"); spinner.text = `Fetching ${trimmed}@${targetVersion}`; - versionResult = await apiRequestPackageVersion(registry, trimmed, targetVersion, token); + versionResult = await apiRequestPackageVersion(registry, trimmed, targetVersion); } let versionsList: Awaited> | null = null; if (options.versions) { const limit = clampLimit(options.limit ?? 25, 100); spinner.text = `Fetching versions (${limit})`; - versionsList = await apiRequestPackageVersions(registry, trimmed, limit, token); + versionsList = await apiRequestPackageVersions(registry, trimmed, limit); } let fileContent: string | null = null; @@ -390,7 +267,7 @@ export async function cmdInspectPackage( url.searchParams.set("version", latestVersion); } spinner.text = `Fetching ${options.file}`; - fileContent = await fetchText(registry, { url: url.toString(), token }); + fileContent = await fetchText(registry, { url: url.toString() }); } spinner.stop(); @@ -462,33 +339,6 @@ export async function cmdInspectPackage( } } -export async function cmdGetPackageTrustedPublisher( - opts: GlobalOpts, - packageName: string, - options: PackageTrustedPublisherGetOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - const token = await getOptionalAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Fetching trusted publisher"); - try { - const result = await apiRequestPackageTrustedPublisher(registry, trimmed, token); - spinner.stop(); - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - if (!result.trustedPublisher) { - console.log("No trusted publisher configured."); - return; - } - printTrustedPublisher(result.trustedPublisher); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - export async function cmdPackPackage( opts: GlobalOpts, sourceArg: string, @@ -615,112 +465,6 @@ async function createClawPackFromFolder(options: { }; } -export async function cmdPublishPackage( - opts: GlobalOpts, - sourceArg: string, - options: PackagePublishOptions = {}, -) { - if (!sourceArg?.trim()) fail("Path required"); - - let plan: PackagePublishPlan | undefined; - try { - plan = await preparePackagePublishPlan(opts, sourceArg, options); - - if (options.dryRun) { - if (options.json) { - process.stdout.write(`${JSON.stringify(plan.output, null, 2)}\n`); - } else { - printPackageDryRun({ - source: plan.sourceLabel, - family: plan.payload.family, - name: plan.payload.name, - displayName: plan.payload.displayName, - version: plan.payload.version, - commit: plan.payload.source?.commit, - compatibility: plan.compatibility, - tags: plan.payload.tags, - files: plan.filesOnDisk, - }); - } - return; - } - - if (plan.payload.family === "code-plugin") { - const validation = validateOpenClawExternalCodePluginPackageContents( - plan.packageJson, - plan.filesOnDisk.map((file) => file.relPath), - ); - if (validation.issues.length > 0) { - fail(validation.issues.map((issue) => issue.message).join(" ")); - } - } - - const registry = await getRegistry(opts, { cache: true }); - const spinner = options.json - ? null - : createSpinner(`Preparing ${plan.payload.name}@${plan.payload.version}`); - try { - const publishToken = await resolvePackagePublishToken({ - registry, - packageName: plan.payload.name, - version: plan.payload.version, - manualOverrideReason: plan.payload.manualOverrideReason, - spinner, - }); - const form = new FormData(); - form.set("payload", JSON.stringify(plan.payload)); - - if (plan.clawpackOnDisk) { - if (spinner) spinner.text = `Uploading ${plan.clawpackOnDisk.relPath}`; - const blob = new Blob([Buffer.from(plan.clawpackOnDisk.bytes)], { - type: "application/octet-stream", - }); - form.append("clawpack", blob, plan.clawpackOnDisk.relPath); - } else { - let index = 0; - for (const file of plan.filesOnDisk) { - index += 1; - if (spinner) { - spinner.text = `Uploading ${file.relPath} (${index}/${plan.filesOnDisk.length})`; - } - const blob = new Blob([Buffer.from(file.bytes)], { - type: file.contentType ?? "application/octet-stream", - }); - form.append("files", blob, file.relPath); - } - } - - if (spinner) spinner.text = `Publishing ${plan.payload.name}@${plan.payload.version}`; - const result = await apiRequestForm( - registry, - { - method: "POST", - path: ApiRoutes.packages, - token: publishToken, - form, - retryCount: PACKAGE_PUBLISH_RETRY_COUNT, - }, - ApiV1PackagePublishResponseSchema, - ); - - if (options.json) { - process.stdout.write( - `${JSON.stringify({ ...plan.output, releaseId: result.releaseId }, null, 2)}\n`, - ); - } else { - spinner?.succeed( - `OK. Published ${plan.payload.name}@${plan.payload.version} (${result.releaseId})`, - ); - } - } catch (error) { - spinner?.fail(formatError(error)); - throw error; - } - } finally { - await plan?.cleanup?.(); - } -} - export async function cmdDownloadPackage( opts: GlobalOpts, packageName: string, @@ -728,22 +472,18 @@ export async function cmdDownloadPackage( ) { const trimmed = normalizePackageNameOrFail(packageName); if (options.version && options.tag) fail("Use either --version or --tag"); - - const token = await getOptionalAuthToken(); const registry = await getRegistry(opts, { cache: true }); const spinner = options.json ? null : createSpinner("Resolving package artifact"); try { const targetVersion = await resolvePackageVersion(registry, trimmed, { - token, version: options.version, tag: options.tag, }); spinnerText(spinner, `Resolving ${trimmed}@${targetVersion}`); - const artifactResult = await apiRequestPackageArtifact(registry, trimmed, targetVersion, token); + const artifactResult = await apiRequestPackageArtifact(registry, trimmed, targetVersion); spinnerText(spinner, `Downloading ${trimmed}@${targetVersion}`); const bytes = await fetchBinary(registry, { url: artifactResult.artifact.downloadUrl, - token, }); const identity = computeArtifactIdentity(bytes); validateDownloadedArtifact(trimmed, artifactResult, bytes, identity); @@ -800,15 +540,13 @@ export async function cmdVerifyPackage( if (options.packageName?.trim()) { const packageName = normalizePackageNameOrFail(options.packageName); - const token = await getOptionalAuthToken(); const registry = await getRegistry(opts, { cache: true }); spinnerText(spinner, `Resolving ${packageName}`); const targetVersion = await resolvePackageVersion(registry, packageName, { - token, version: options.version, tag: options.tag, }); - artifactResult = await apiRequestPackageArtifact(registry, packageName, targetVersion, token); + artifactResult = await apiRequestPackageArtifact(registry, packageName, targetVersion); validateDownloadedArtifact(packageName, artifactResult, bytes, identity); } @@ -858,223 +596,18 @@ export async function cmdVerifyPackage( } } -export async function cmdDeletePackage( - opts: GlobalOpts, - nameArg: string, - options: PackageDeleteOptions = {}, - inputAllowed = true, -) { - const name = nameArg.trim(); - if (!name) fail("Package name required"); - - if (!options.yes) { - if (!isInteractive() || inputAllowed === false) fail("Pass --yes (no input)"); - const ok = await promptConfirm(`Delete ${name}? (soft delete package and all releases)`); - if (!ok) return undefined; - } - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Deleting ${name}`); - try { - const result = await apiRequest( - registry, - { - method: "DELETE", - path: `${ApiRoutes.packages}/${encodeURIComponent(name)}`, - token, - }, - ApiV1DeleteResponseSchema, - ); - spinner.succeed(`OK. Deleted ${name}`); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - } - return result; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdUndeletePackage( - opts: GlobalOpts, - nameArg: string, - options: PackageUndeleteOptions = {}, - inputAllowed = true, -) { - const name = nameArg.trim(); - if (!name) fail("Package name required"); - - if (!options.yes) { - if (!isInteractive() || inputAllowed === false) fail("Pass --yes (no input)"); - const ok = await promptConfirm(`Restore ${name}? (restore package and releases)`); - if (!ok) return undefined; - } - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Restoring ${name}`); - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/undelete`, - token, - }, - ApiV1DeleteResponseSchema, - ); - spinner.succeed(`OK. Restored ${name}`); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - } - return result; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdTransferPackage( - opts: GlobalOpts, - nameArg: string, - options: PackageTransferOptions, -) { - const name = normalizePackageNameOrFail(nameArg); - const toOwner = options.to?.trim().replace(/^@+/, "").toLowerCase(); - if (!toOwner) fail("--to required"); - const reason = options.reason?.trim(); - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Transferring ${name} to @${toOwner}`); - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/transfer`, - token, - body: { - toOwner, - ...(reason ? { reason } : {}), - }, - }, - ApiV1PackageTransferResponseSchema, - ); - spinner.succeed(`OK. Transferred ${name} to @${toOwner}`); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - } - return result; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdReportPackage( - opts: GlobalOpts, - packageName: string, - options: PackageReportOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - const reason = options.reason?.trim(); - const version = options.version?.trim(); - if (!reason) fail("--reason required"); - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = options.json ? null : createSpinner(`Reporting ${trimmed}`); - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/report`, - token, - body: { - reason, - ...(version ? { version } : {}), - }, - }, - ApiV1PackageReportResponseSchema, - ); - spinner?.stop(); - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - if (result.alreadyReported) { - console.log(`Already reported ${trimmed}.`); - return; - } - const versionSuffix = version ? `@${version}` : ""; - console.log(`OK. Reported ${trimmed}${versionSuffix} for moderator review.`); - } catch (error) { - spinner?.fail(formatError(error)); - throw error; - } -} - -export async function cmdPackageModerationStatus( - opts: GlobalOpts, - packageName: string, - options: PackageModerationStatusOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const result = await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/moderation`, - token, - }, - ApiV1PackageModerationStatusResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - - console.log(`${result.package.name} moderation`); - console.log(` package scan: ${result.package.scanStatus ?? "unknown"}`); - console.log(` open reports: ${result.package.reportCount}`); - if (!result.latestRelease) { - console.log(" latest release: none"); - return; - } - const state = result.latestRelease.moderationState ?? "none"; - console.log(` latest: ${result.latestRelease.version}`); - console.log(` release scan: ${result.latestRelease.scanStatus}`); - console.log(` manual state: ${state}`); - console.log(` blocked: ${result.latestRelease.blockedFromDownload ? "yes" : "no"}`); - if (result.latestRelease.reasons.length > 0) { - console.log(` reasons: ${result.latestRelease.reasons.join(", ")}`); - } - if (result.latestRelease.moderationReason) { - console.log(` note: ${result.latestRelease.moderationReason}`); - } -} - export async function cmdPackageReadiness( opts: GlobalOpts, packageName: string, options: PackageReadinessOptions = {}, ) { const trimmed = normalizePackageNameOrFail(packageName); - const token = await getOptionalAuthToken(); const registry = await getRegistry(opts, { cache: true }); const result = await apiRequest( registry, { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/readiness`, - token, }, ApiV1PackageReadinessResponseSchema, ); @@ -1099,14 +632,12 @@ export async function cmdPackageMigrationStatus( options: PackageMigrationStatusOptions = {}, ) { const trimmed = normalizePackageNameOrFail(packageName); - const token = await getOptionalAuthToken(); const registry = await getRegistry(opts, { cache: true }); const result = await apiRequest( registry, { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/readiness`, - token, }, ApiV1PackageReadinessResponseSchema, ); @@ -1128,10 +659,10 @@ export async function cmdPackageMigrationStatus( } } -async function apiRequestPackageDetail(registry: string, name: string, token?: string) { +async function apiRequestPackageDetail(registry: string, name: string) { return await apiRequest( registry, - { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(name)}`, token }, + { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(name)}` }, ApiV1PackageResponseSchema, ); } @@ -1140,43 +671,27 @@ async function apiRequestPackageArtifact( registry: string, name: string, version: string, - token?: string, ) { return await apiRequest( registry, { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}/artifact`, - token, }, ApiV1PackageArtifactResponseSchema, ); } -async function apiRequestPackageTrustedPublisher(registry: string, name: string, token?: string) { - return await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/trusted-publisher`, - token, - }, - ApiV1PackageTrustedPublisherResponseSchema, - ); -} - async function apiRequestPackageVersion( registry: string, name: string, version: string, - token?: string, ) { return await apiRequest( registry, { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`, - token, }, ApiV1PackageVersionResponseSchema, ); @@ -1186,13 +701,12 @@ async function apiRequestPackageVersions( registry: string, name: string, limit: number, - token?: string, ) { const url = registryUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/versions`, registry); url.searchParams.set("limit", String(limit)); return await apiRequest( registry, - { method: "GET", url: url.toString(), token }, + { method: "GET", url: url.toString() }, ApiV1PackageVersionListResponseSchema, ); } @@ -1200,10 +714,10 @@ async function apiRequestPackageVersions( async function resolvePackageVersion( registry: string, name: string, - args: { token?: string; version?: string; tag?: string }, + args: { version?: string; tag?: string }, ) { if (args.version?.trim()) return args.version.trim(); - const detail = await apiRequestPackageDetail(registry, name, args.token); + const detail = await apiRequestPackageDetail(registry, name); if (!detail.package) fail("Package not found"); const tags = normalizeTags(detail.package.tags); if (args.tag?.trim()) { @@ -1371,30 +885,12 @@ function printVersionSummary(version: NonNullable 0) console.log(`Compatibility: ${entries.join(", ")}`); -} - -function formatCompatibilityEntries(compatibility: PackageCompatibility) { - return [ - compatibility.pluginApiRange ? `pluginApi=${compatibility.pluginApiRange}` : null, - compatibility.builtWithOpenClawVersion - ? `builtWith=${compatibility.builtWithOpenClawVersion}` - : null, - compatibility.pluginSdkVersion ? `sdk=${compatibility.pluginSdkVersion}` : null, - compatibility.minGatewayVersion ? `minGateway=${compatibility.minGatewayVersion}` : null, - ].filter(Boolean); + const entries = Object.entries(compatibility) + .filter(([, value]) => value !== undefined && value !== null) + .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(",") : String(value)}`); + if (entries.length > 0) console.log(`Compatibility: ${entries.join("; ")}`); } function printCapabilities(capabilities: PackageCapabilitySummary | null | undefined) { @@ -1529,547 +1025,8 @@ function assertClawPackSize(size: number, label: string) { } } -const REAL_BUNDLE_MANIFESTS = [ - { path: ".codex-plugin/plugin.json", format: "codex" }, - { path: ".claude-plugin/plugin.json", format: "claude" }, - { path: ".cursor-plugin/plugin.json", format: "cursor" }, -] as const; - -function hasRealBundleMarker(fileSet: Set) { - return ( - REAL_BUNDLE_MANIFESTS.some((marker) => fileSet.has(marker.path)) || - Array.from(fileSet).some( - (path) => - path.startsWith("skills/") || - path.startsWith("commands/") || - path.startsWith("agents/") || - path === "hooks/hooks.json" || - path === ".mcp.json" || - path === ".lsp.json" || - path === "settings.json", - ) - ); -} - -function detectPackageFamily( - fileSet: Set, - explicit?: "code-plugin" | "bundle-plugin", -): "code-plugin" | "bundle-plugin" { - if (explicit) return explicit; - if (hasRealBundleMarker(fileSet)) return "bundle-plugin"; - if (fileSet.has("openclaw.plugin.json")) return "code-plugin"; - return fail("Could not detect package family. Use --family."); -} - -async function readBundleManifestInfo( - filesOnDisk: PackageFile[], - folder: string, - parsedClawpack: ReturnType | undefined, -) { - for (const marker of REAL_BUNDLE_MANIFESTS) { - const manifest = - readJsonEntry(filesOnDisk, marker.path) ?? - (parsedClawpack ? null : await readJsonFile(join(folder, marker.path))); - if (manifest) return { manifest, format: marker.format }; - } - return { manifest: null, format: undefined }; -} - -function parseTags(value: string) { - return value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function parseCsv(value: string | undefined) { - if (!value) return []; - return value - .split(",") - .map((entry) => entry.trim()) - .filter(Boolean); -} - -function applyGitHubSourcePath( - source: Awaited>, - sourcePath: string | undefined, -) { - const explicitPath = sourcePath?.trim(); - if (!explicitPath || source.kind !== "github") return source; - return { ...source, path: explicitPath }; -} - -async function preparePackagePublishPlan( - opts: GlobalOpts, - sourceArg: string, - options: PackagePublishOptions, -): Promise { - const resolvedSource = await resolveSourceInput(sourceArg, { - workdir: opts.workdir, - localWorkdirs: [process.cwd(), opts.workdir], - }); - const sourceForFetch = applyGitHubSourcePath(resolvedSource, options.sourcePath); - let folder = sourceForFetch.kind === "local" ? sourceForFetch.path : ""; - let cleanup: (() => Promise) | undefined; - let inferredSource: InferredPublishSource | undefined; - let clawpackOnDisk: PackageFile | undefined; - let parsedClawpack: ReturnType | undefined; - const addCleanup = (next: () => Promise) => { - const previous = cleanup; - cleanup = async () => { - await next(); - await previous?.(); - }; - }; - - if (sourceForFetch.kind === "github") { - const fetchSpinner = options.json - ? null - : createSpinner(`Fetching ${sourceForFetch.owner}/${sourceForFetch.repo}`); - try { - const fetched = await fetchGitHubSource(sourceForFetch); - folder = fetched.dir; - cleanup = fetched.cleanup; - inferredSource = fetched.source; - fetchSpinner?.stop(); - } catch (error) { - fetchSpinner?.fail(formatError(error)); - throw error; - } - } else { - const folderStat = await stat(folder).catch(() => null); - if (!folderStat) fail("Path must be a folder or ClawPack .tgz"); - if (folderStat.isFile()) { - if (!folder.endsWith(".tgz")) fail("ClawPack publish files must end in .tgz"); - const bytes = new Uint8Array(await readFile(folder)); - assertClawPackSize(bytes.byteLength, basename(folder)); - parsedClawpack = parseClawPack(bytes); - clawpackOnDisk = { - relPath: basename(folder), - bytes, - contentType: "application/octet-stream", - }; - } else if (!folderStat.isDirectory()) { - fail("Path must be a folder or ClawPack .tgz"); - } - - const localGitInfo = folderStat.isDirectory() ? resolveLocalGitInfo(folder) : null; - if (localGitInfo) { - inferredSource = { - repo: localGitInfo.repo, - commit: localGitInfo.commit, - ref: localGitInfo.ref, - path: localGitInfo.path, - ...(localGitInfo.repo ? { url: `https://github.com/${localGitInfo.repo}` } : {}), - }; - } - } - - let filesOnDisk = parsedClawpack - ? parsedClawpack.entries.map((entry) => ({ - relPath: entry.path, - bytes: entry.bytes, - contentType: mime.getType(entry.path) ?? "application/octet-stream", - })) - : await listPackageFiles(folder); - if (filesOnDisk.length === 0) fail("No files found"); - - const fileSet = new Set(filesOnDisk.map((file) => file.relPath.toLowerCase())); - const packageJson = - parsedClawpack?.packageJson ?? (await readJsonFile(join(folder, "package.json"))); - const pluginManifest = - readJsonEntry(filesOnDisk, "openclaw.plugin.json") ?? - (parsedClawpack ? null : await readJsonFile(join(folder, "openclaw.plugin.json"))); - const bundleManifestInfo = await readBundleManifestInfo(filesOnDisk, folder, parsedClawpack); - const bundleManifest = bundleManifestInfo.manifest; - const family = detectPackageFamily(fileSet, options.family); - const name = - options.name?.trim() || - parsedClawpack?.packageName || - packageJsonString(packageJson, "name") || - packageJsonString(pluginManifest, "id") || - packageJsonString(bundleManifest, "id") || - basename(folder).trim().toLowerCase(); - const displayName = - options.displayName?.trim() || - packageJsonString(packageJson, "displayName") || - packageJsonString(pluginManifest, "name") || - packageJsonString(bundleManifest, "name") || - titleCase(basename(folder)); - const ownerHandle = options.owner?.trim().replace(/^@+/, ""); - const version = - options.version?.trim() || - parsedClawpack?.packageVersion || - packageJsonString(packageJson, "version"); - const changelog = options.changelog ?? ""; - let clawScanNote: string | undefined; - try { - clawScanNote = normalizeClawScanNote(options.clawscanNote); - } catch (error) { - fail(formatError(error)); - } - const tags = parseTags(options.tags ?? "latest"); - const source = buildSource(options, inferredSource); - - if (!name) fail("--name required"); - if (!displayName) fail("--display-name required"); - if (!version) fail("--version required"); - if (!fileSet.has("openclaw.plugin.json")) fail("openclaw.plugin.json required"); - if (family === "code-plugin" && !semver.valid(version)) { - fail("--version must be valid semver for code plugins"); - } - if (family === "code-plugin") { - if (!fileSet.has("package.json")) fail("package.json required"); - if (!source) fail("--source-repo and --source-commit required for code plugins"); - const validation = validateOpenClawExternalCodePluginPackageJson(packageJson); - if (validation.issues.length > 0) { - fail(validation.issues.map((issue) => issue.message).join(" ")); - } - } - - if (family === "code-plugin" && !clawpackOnDisk) { - const packDestination = await mkdtemp(join(tmpdir(), "clawhub-clawpack-")); - let packed: PackedClawPack; - try { - packed = await createClawPackFromFolder({ - sourcePath: folder, - packDestination, - cwd: opts.workdir, - }); - if (packed.parsed.packageName !== name) { - fail(`ClawPack package name mismatch: expected ${name}, got ${packed.parsed.packageName}`); - } - if (packed.parsed.packageVersion !== version) { - fail( - `ClawPack package version mismatch: expected ${version}, got ${packed.parsed.packageVersion}`, - ); - } - } catch (error) { - await rm(packDestination, { recursive: true, force: true }); - throw error; - } - addCleanup(async () => { - await rm(packDestination, { recursive: true, force: true }); - }); - parsedClawpack = packed.parsed; - clawpackOnDisk = packed.file; - filesOnDisk = packed.parsed.entries.map((entry) => ({ - relPath: entry.path, - bytes: entry.bytes, - contentType: mime.getType(entry.path) ?? "application/octet-stream", - })); - } - - const payload: PackagePublishPayload = { - name, - displayName, - ...(ownerHandle ? { ownerHandle } : {}), - family, - version, - changelog, - ...(clawScanNote ? { clawScanNote } : {}), - ...(options.manualOverrideReason?.trim() - ? { manualOverrideReason: options.manualOverrideReason.trim() } - : {}), - tags, - ...(source ? { source } : {}), - ...(family === "bundle-plugin" - ? { - bundle: { - format: options.bundleFormat?.trim() || bundleManifestInfo.format, - hostTargets: parseCsv(options.hostTargets), - }, - } - : {}), - }; - const sourceLabel = describePublishSource(sourceForFetch, source, folder); - - return { - folder, - cleanup, - filesOnDisk, - clawpackOnDisk, - packageJson, - payload, - compatibility: - family === "code-plugin" - ? normalizeOpenClawExternalPluginCompatibility(packageJson) - : undefined, - sourceLabel, - output: { - source: sourceLabel, - name, - displayName, - family, - version, - ...(source?.commit ? { commit: source.commit } : {}), - files: filesOnDisk.length, - totalBytes: clawpackOnDisk - ? clawpackOnDisk.bytes.byteLength - : filesOnDisk.reduce((sum, file) => sum + file.bytes.byteLength, 0), - }, - }; -} - -function readJsonEntry(files: PackageFile[], path: string) { - const file = files.find((entry) => entry.relPath.toLowerCase() === path.toLowerCase()); - if (!file) return null; - try { - const parsed = JSON.parse(new TextDecoder().decode(file.bytes)) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : null; - } catch { - return null; - } -} - -function hasGitHubActionsOidcEnv(env: NodeJS.ProcessEnv = process.env) { - return Boolean(env.ACTIONS_ID_TOKEN_REQUEST_URL && env.ACTIONS_ID_TOKEN_REQUEST_TOKEN); -} - -async function requestGitHubActionsOidcToken( - audience: string, - options: { - env?: NodeJS.ProcessEnv; - fetchImpl?: typeof fetch; - } = {}, -) { - const env = options.env ?? process.env; - const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis); - const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL?.trim(); - const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN?.trim(); - if (!requestUrl || !requestToken) { - throw new Error("GitHub Actions OIDC is not available in this environment."); - } - - const url = new URL(requestUrl); - url.searchParams.set("audience", audience); - const response = await fetchImpl(url, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: `Bearer ${requestToken}`, - }, - }); - const responseText = await response.text(); - if (!response.ok) { - throw new Error( - `GitHub OIDC token request failed (${response.status}): ${responseText || response.statusText}`, - ); - } - - let parsed: unknown; - try { - parsed = JSON.parse(responseText); - } catch { - throw new Error("GitHub OIDC token request returned invalid JSON."); - } - - const token = (parsed as { value?: unknown }).value; - if (typeof token !== "string" || !token.trim()) { - throw new Error("GitHub OIDC token response did not include a token value."); - } - return token; -} - -async function mintPackagePublishToken( - registry: string, - packageName: string, - version: string, - githubOidcToken: string, -) { - const response = await apiRequest( - registry, - { - method: "POST", - path: ApiRoutes.publishTokenMint, - body: { - packageName, - version, - githubOidcToken, - }, - }, - ApiV1PublishTokenMintResponseSchema, - ); - return response.token; -} - -async function resolvePackagePublishToken(params: { - registry: string; - packageName: string; - version: string; - manualOverrideReason?: string; - spinner: ReturnType | null; -}) { - if (params.manualOverrideReason?.trim()) { - return await requireAuthToken(); - } - - if (!hasGitHubActionsOidcEnv()) { - return await requireAuthToken(); - } - - if (params.spinner) { - params.spinner.text = "Requesting GitHub Actions OIDC token"; - } - try { - const githubOidcToken = await requestGitHubActionsOidcToken("clawhub"); - if (params.spinner) { - params.spinner.text = "Minting short-lived ClawHub publish token"; - } - return await mintPackagePublishToken( - params.registry, - params.packageName, - params.version, - githubOidcToken, - ); - } catch (error) { - const status = - typeof error === "object" && error !== null && "status" in error - ? (error as { status?: unknown }).status - : undefined; - if (status !== undefined && status !== 400 && status !== 403 && status !== 404) { - throw error; - } - if (params.spinner) { - params.spinner.text = "Trusted publishing unavailable, falling back to ClawHub token"; - } - return await requireAuthToken(); - } -} - -function buildSource(options: PackagePublishOptions, inferred?: InferredPublishSource) { - const rawRepo = options.sourceRepo?.trim() || inferred?.repo?.trim(); - const rawCommit = options.sourceCommit?.trim() || inferred?.commit?.trim(); - const rawRef = options.sourceRef?.trim() || inferred?.ref?.trim(); - const explicitPath = options.sourcePath?.trim(); - const rawPath = explicitPath !== undefined ? explicitPath : inferred?.path?.trim(); - if (!rawRepo && !rawCommit && !rawRef && !rawPath) return undefined; - if (!rawRepo || !rawCommit) fail("--source-repo and --source-commit must be set together"); - const repo = normalizeGitHubRepo(rawRepo); - if (!repo) fail("--source-repo must be a GitHub repo or URL"); - const explicitRepo = options.sourceRepo?.trim(); - const url = explicitRepo - ? explicitRepo.startsWith("http") - ? explicitRepo - : `https://github.com/${repo}` - : inferred?.url || `https://github.com/${repo}`; - return { - kind: "github" as const, - url, - repo, - ref: rawRef || rawCommit, - commit: rawCommit, - path: rawPath || ".", - importedAt: Date.now(), - }; -} - -function describePublishSource( - sourceInput: Awaited>, - source: ReturnType, - folder: string, -) { - if (source) { - return `github:${source.repo}@${source.ref}${source.path !== "." ? `:${source.path}` : ""}`; - } - if (sourceInput.kind === "github") { - const repo = `${sourceInput.owner}/${sourceInput.repo}`; - return `github:${repo}@${sourceInput.ref ?? "HEAD"}${ - sourceInput.path !== "." ? `:${sourceInput.path}` : "" - }`; - } - return `local:${folder}`; -} - -function printPackageDryRun(params: { - source: string; - family: PackageFamily; - name: string; - displayName: string; - version: string; - commit?: string; - compatibility?: PackageCompatibility; - tags: string[]; - files: PackageFile[]; -}) { - console.log("Dry run - nothing will be published."); - console.log(""); - console.log(`Source: ${params.source}`); - console.log(`Family: ${params.family}`); - console.log(`Name: ${params.name}`); - console.log(`Display: ${params.displayName}`); - console.log(`Version: ${params.version}`); - if (params.commit) console.log(`Commit: ${params.commit}`); - if (params.compatibility) { - console.log(`Compat: ${formatCompatibilityEntries(params.compatibility).join(", ")}`); - } - console.log( - `Files: ${params.files.length} files (${formatByteCount( - params.files.reduce((sum, file) => sum + file.bytes.byteLength, 0), - )})`, - ); - console.log(`Tags: ${params.tags.join(", ")}`); - console.log(""); - console.log("Files:"); - for (const file of params.files) { - console.log(` ${file.relPath.padEnd(28)} ${formatByteCount(file.bytes.byteLength)}`); - } -} - function formatByteCount(value: number) { if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; return `${(value / (1024 * 1024)).toFixed(1)} MB`; } - -async function listPackageFiles(root: string) { - const files: PackageFile[] = []; - const absRoot = resolve(root); - const ig = ignore(); - ig.add([".git/", "node_modules/", `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`]); - await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); - await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE)); - await walk(absRoot, async (absPath) => { - const relPath = normalizePath(relative(absRoot, absPath)); - if (!relPath || ig.ignores(relPath)) return; - const bytes = new Uint8Array(await readFile(absPath)); - files.push({ - relPath, - bytes, - contentType: mime.getType(relPath) ?? "application/octet-stream", - }); - }); - return files; -} - -function normalizePath(path: string) { - return path - .split(sep) - .join("/") - .replace(/^\.\/+/, ""); -} - -async function walk(dir: string, onFile: (path: string) => Promise) { - const entries = await readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name === ".git" || entry.name === "node_modules") continue; - const full = join(dir, entry.name); - if (entry.isDirectory()) { - await walk(full, onFile); - continue; - } - if (!entry.isFile()) continue; - await onFile(full); - } -} - -async function addIgnoreFile(ig: ReturnType, path: string) { - try { - const raw = await readFile(path, "utf8"); - ig.add(raw.split(/\r?\n/)); - } catch { - // optional - } -} diff --git a/dt-skill/src/cli/commands/publish.test.ts b/dt-skill/src/cli/commands/publish.test.ts index 6d952e46..547b6c4f 100644 --- a/dt-skill/src/cli/commands/publish.test.ts +++ b/dt-skill/src/cli/commands/publish.test.ts @@ -4,23 +4,19 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, +import { createHttpModuleMocks, createRegistryModuleMocks, createUiModuleMocks, makeGlobalOpts, } from "../../../test/cliCommandTestKit.js"; import { MAX_CLAWSCAN_NOTE_CHARS } from "../../schema/index.js"; -const authTokenMocks = createAuthTokenModuleMocks(); const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks({ interactive: true }); const mockSearchMultiselect = vi.fn(); -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); vi.mock("../registry.js", () => registryMocks.moduleFactory()); vi.mock("../../http.js", () => httpMocks.moduleFactory()); vi.mock("../ui.js", () => uiMocks.moduleFactory()); @@ -79,7 +75,6 @@ describe("cmdPublish", () => { return req?.path === "/api/v1/skills"; }); if (!publishCall) throw new Error("Missing publish call"); - expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); expect(publishCall[1]).not.toHaveProperty("token"); const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; const payloadEntry = publishForm.get("payload"); @@ -246,7 +241,6 @@ describe("cmdPublish", () => { ).rejects.toThrow( 'This looks like a plugin. Use "clawhub package publish " instead.', ); - expect(authTokenMocks.requireAuthToken).not.toHaveBeenCalled(); expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); } finally { await rm(workdir, { recursive: true, force: true }); diff --git a/dt-skill/src/cli/commands/publishers.test.ts b/dt-skill/src/cli/commands/publishers.test.ts deleted file mode 100644 index 075f6fad..00000000 --- a/dt-skill/src/cli/commands/publishers.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* @vitest-environment node */ - -import { describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, - createRegistryModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; - -const authTokenMocks = createAuthTokenModuleMocks(); -const registryMocks = createRegistryModuleMocks(); -const httpMocks = createHttpModuleMocks(); - -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); - -const { cmdCreatePublisher } = await import("./publishers"); - -const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); -const mockWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); - -function makeOpts(workdir = "/work") { - return makeGlobalOpts(workdir); -} - -describe("publisher CLI commands", () => { - it("creates an org publisher through the v1 publishers API", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - publisherId: "publishers:opik", - handle: "opik", - created: true, - trusted: false, - }); - - await cmdCreatePublisher(makeOpts(), "Opik", { displayName: "Opik" }); - - expect(authTokenMocks.requireAuthToken).toHaveBeenCalled(); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - expect.objectContaining({ - method: "POST", - path: "/api/v1/publishers", - token: "tkn", - body: { handle: "opik", displayName: "Opik" }, - }), - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("OK. Created publisher @opik."); - }); - - it("prints JSON for created org publishers", async () => { - const response = { - ok: true, - publisherId: "publishers:opik", - handle: "opik", - created: true, - trusted: false, - }; - httpMocks.apiRequest.mockResolvedValueOnce(response); - - await cmdCreatePublisher(makeOpts(), "opik", { json: true }); - - expect(mockWrite).toHaveBeenCalledWith(`${JSON.stringify(response, null, 2)}\n`); - }); -}); diff --git a/dt-skill/src/cli/commands/publishers.ts b/dt-skill/src/cli/commands/publishers.ts deleted file mode 100644 index df833926..00000000 --- a/dt-skill/src/cli/commands/publishers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { apiRequest } from "../../http.js"; -import { ApiRoutes, ApiV1PublisherCreateResponseSchema } from "../../schema/index.js"; -import { requireAuthToken } from "../authToken.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; - -type PublisherCreateOptions = { - displayName?: string; - json?: boolean; -}; - -function normalizePublisherHandleOrFail(handle: string) { - const normalized = handle.trim().replace(/^@+/, "").toLowerCase(); - if (!normalized) throw new Error("Publisher handle is required"); - return normalized; -} - -export async function cmdCreatePublisher( - opts: GlobalOpts, - handle: string, - options: PublisherCreateOptions = {}, -) { - const normalizedHandle = normalizePublisherHandleOrFail(handle); - const displayName = options.displayName?.trim(); - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const result = await apiRequest( - registry, - { - method: "POST", - path: ApiRoutes.publishers, - token, - body: { - handle: normalizedHandle, - ...(displayName ? { displayName } : {}), - }, - }, - ApiV1PublisherCreateResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - - console.log(`OK. Created publisher @${result.handle}.`); -} diff --git a/dt-skill/src/cli/commands/skills.install.test.ts b/dt-skill/src/cli/commands/skills.install.test.ts index ffb9998c..e06a28a5 100644 --- a/dt-skill/src/cli/commands/skills.install.test.ts +++ b/dt-skill/src/cli/commands/skills.install.test.ts @@ -2,9 +2,7 @@ import * as fsPromises from "node:fs/promises"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, +import { createHttpModuleMocks, createRegistryModuleMocks, createUiModuleMocks, makeGlobalOpts, @@ -30,13 +28,11 @@ vi.mock("node:fs/promises", async () => { const mocked = (value: T) => value as T & Record; Object.assign(vi as object, { mocked }); -const authTokenMocks = createAuthTokenModuleMocks(); const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks(); const mockApiRequest = httpMocks.apiRequest; const mockDownloadZip = httpMocks.downloadZip; -const mockGetOptionalAuthToken = authTokenMocks.getOptionalAuthToken; const mockSpinner = uiMocks.spinner; const mockIsInteractive = vi.fn(() => false); const mockPromptConfirm = vi.fn(async () => false); @@ -47,7 +43,6 @@ const mockSearchMultiselect = vi.fn(); vi.mock("../../http.js", () => httpMocks.moduleFactory()); vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); vi.mock("../ui.js", () => ({ createSpinner: vi.fn(() => mockSpinner), fail: (message: string) => uiMocks.fail(message), @@ -97,7 +92,6 @@ beforeEach(() => { readSkillOriginMock.mockResolvedValue(null); writeLockfileMock.mockResolvedValue(undefined); writeSkillOriginMock.mockResolvedValue(undefined); - mockGetOptionalAuthToken.mockResolvedValue(undefined); }); afterEach(() => { @@ -116,7 +110,6 @@ afterAll(() => { describe("cmdInstall with packages", () => { it("installs single non-package skill directly", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockResolvedValue({ skill: { slug: "single-skill", @@ -143,7 +136,6 @@ describe("cmdInstall with packages", () => { }); it("installs multiple skills in batch", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockImplementation(async (_registry, request) => { const slug = (request as any).path?.split("/").pop(); return { @@ -162,7 +154,6 @@ describe("cmdInstall with packages", () => { }); it("installs skill when latestVersion is null", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockResolvedValue({ skill: { slug: "no-version-skill", @@ -189,7 +180,6 @@ describe("cmdInstall with packages", () => { }); it("continues batch install when one skill fails", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); let callCount = 0; mockApiRequest.mockImplementation(async (_registry, request) => { callCount++; @@ -213,7 +203,6 @@ describe("cmdInstall with packages", () => { }); it("continues batch install when fail() is triggered for one skill", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockImplementation(async (_registry, request) => { const slug = (request as any).path?.split("/").pop(); return { @@ -343,7 +332,6 @@ describe("cmdInstall with packages", () => { workdir: "/mock/.claude", dir: "/mock/.claude/skills", }); - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockImplementation(async (_registry, request) => { const slug = (request as any).path?.split("/").pop(); return { diff --git a/dt-skill/src/cli/commands/skills.test.ts b/dt-skill/src/cli/commands/skills.test.ts index fc302b5e..d1fe25e6 100644 --- a/dt-skill/src/cli/commands/skills.test.ts +++ b/dt-skill/src/cli/commands/skills.test.ts @@ -2,9 +2,7 @@ import * as fsPromises from "node:fs/promises"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, +import { createHttpModuleMocks, createRegistryModuleMocks, createUiModuleMocks, makeGlobalOpts, @@ -31,19 +29,16 @@ vi.mock("node:fs/promises", async () => { const mocked = (value: T) => value as T & Record; Object.assign(vi as object, { mocked }); -const authTokenMocks = createAuthTokenModuleMocks(); const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks(); const mockApiRequest = httpMocks.apiRequest; const mockDownloadZip = httpMocks.downloadZip; -const mockGetOptionalAuthToken = authTokenMocks.getOptionalAuthToken; const mockSpinner = uiMocks.spinner; const mockIsInteractive = vi.fn(() => false); const mockPromptConfirm = vi.fn(async () => false); vi.mock("../../http.js", () => httpMocks.moduleFactory()); vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); const mockSelectAgent = vi.fn(async () => null); vi.mock("../ui.js", () => ({ createSpinner: vi.fn(() => mockSpinner), @@ -69,13 +64,7 @@ const { clampLimit, cmdExplore, cmdInstall, - cmdList, - cmdListSkillReports, - cmdPin, - cmdReportSkill, - cmdSearch, - cmdTriageSkillReport, - cmdUninstall, + cmdList, cmdPin, cmdSearch, cmdUninstall, cmdUnpin, cmdUpdate, formatExploreLine, @@ -155,13 +144,11 @@ describe("explore helpers", () => { describe("cmdExplore", () => { it("does not attach a stored auth token to apiRequest", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockResolvedValue({ items: [] }); await cmdExplore(makeOpts(), { limit: 25 }); const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - expect(mockGetOptionalAuthToken).not.toHaveBeenCalled(); expect(requestArgs?.token).toBeUndefined(); }); @@ -227,18 +214,15 @@ describe("cmdExplore", () => { describe("cmdSearch", () => { it("does not attach a stored auth token to apiRequest", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockResolvedValue({ results: [] }); await cmdSearch(makeOpts(), "demo"); const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - expect(mockGetOptionalAuthToken).not.toHaveBeenCalled(); expect(requestArgs?.token).toBeUndefined(); }); it("defaults limit to 25 when not specified", async () => { - mockGetOptionalAuthToken.mockResolvedValue(undefined); mockApiRequest.mockResolvedValue({ results: [] }); await cmdSearch(makeOpts(), "stock price"); @@ -249,7 +233,6 @@ describe("cmdSearch", () => { }); it("uses explicit limit when provided", async () => { - mockGetOptionalAuthToken.mockResolvedValue(undefined); mockApiRequest.mockResolvedValue({ results: [] }); await cmdSearch(makeOpts(), "stock price", 5); @@ -260,7 +243,6 @@ describe("cmdSearch", () => { }); it("prints skill owners in search results", async () => { - mockGetOptionalAuthToken.mockResolvedValue(undefined); mockApiRequest.mockResolvedValue({ results: [ { @@ -287,99 +269,6 @@ describe("cmdSearch", () => { }); }); -describe("skill moderation commands", () => { - it("submits skill reports", async () => { - mockApiRequest.mockResolvedValueOnce({ - ok: true, - reported: true, - alreadyReported: false, - reportId: "skillReports:1", - skillId: "skills:1", - reportCount: 1, - }); - - await cmdReportSkill(makeOpts(), "demo", { version: "1.0.0", reason: "suspicious files" }); - - expect(mockApiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/skills/demo/report", - token: "tkn", - body: { reason: "suspicious files", version: "1.0.0" }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith("OK. Reported demo (skillReports:1)."); - }); - - it("lists skill reports", async () => { - mockApiRequest.mockResolvedValueOnce({ - items: [ - { - reportId: "skillReports:1", - skillId: "skills:1", - skillVersionId: "skillVersions:1", - slug: "demo", - displayName: "Demo", - version: "1.0.0", - reason: "suspicious", - status: "open", - createdAt: 1, - reporter: { userId: "users:reporter", handle: "reporter", displayName: "Reporter" }, - triagedAt: null, - triagedBy: null, - triageNote: null, - }, - ], - nextCursor: null, - done: true, - }); - - await cmdListSkillReports(makeOpts(), { status: "open", limit: 10 }); - - const request = mockApiRequest.mock.calls[0]?.[1] as { url?: string } | undefined; - const url = new URL(String(request?.url)); - expect(url.pathname).toBe("/api/v1/skills/-/reports"); - expect(url.searchParams.get("status")).toBe("open"); - expect(url.searchParams.get("limit")).toBe("10"); - expect(mockLog).toHaveBeenCalledWith("skillReports:1 open demo"); - }); - - it("triages skill reports", async () => { - mockApiRequest.mockResolvedValueOnce({ - ok: true, - reportId: "skillReports:1", - skillId: "skills:1", - status: "confirmed", - reportCount: 0, - actionTaken: "hide", - }); - - await cmdTriageSkillReport(makeOpts(), "skillReports:1", { - status: "confirmed", - note: "handled", - action: "hide", - yes: true, - }); - - expect(mockApiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", - { - method: "POST", - path: "/api/v1/skills/-/reports/skillReports%3A1/triage", - token: "tkn", - body: { status: "confirmed", note: "handled", finalAction: "hide" }, - }, - expect.anything(), - ); - expect(mockLog).toHaveBeenCalledWith( - "OK. Skill report skillReports:1 set to confirmed; action hide.", - ); - expect(mockLog).toHaveBeenCalledWith(" - Hide the skill from public availability."); - }); -}); - describe("cmdUpdate", () => { it("fails when directly updating a pinned skill", async () => { vi.mocked(readLockfile).mockResolvedValue({ @@ -591,7 +480,6 @@ describe("cmdList", () => { describe("cmdInstall", () => { it("does not attach a stored auth token to API or download requests", async () => { - mockGetOptionalAuthToken.mockResolvedValue("tkn"); mockApiRequest.mockResolvedValue({ skill: { slug: "demo", @@ -617,7 +505,6 @@ describe("cmdInstall", () => { await cmdInstall(makeOpts(), "demo"); const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - expect(mockGetOptionalAuthToken).not.toHaveBeenCalled(); expect(requestArgs?.token).toBeUndefined(); const [, zipArgs] = mockDownloadZip.mock.calls[0] ?? []; expect(zipArgs?.token).toBeUndefined(); diff --git a/dt-skill/src/cli/commands/skills.ts b/dt-skill/src/cli/commands/skills.ts index d0966529..40c6f275 100644 --- a/dt-skill/src/cli/commands/skills.ts +++ b/dt-skill/src/cli/commands/skills.ts @@ -6,9 +6,6 @@ import { ApiRoutes, ApiV1SearchResponseSchema, ApiV1SkillListResponseSchema, - ApiV1SkillReportListResponseSchema, - ApiV1SkillReportResponseSchema, - ApiV1SkillReportTriageResponseSchema, ApiV1SkillResolveResponseSchema, ApiV1SkillResponseSchema, ApiV1SkillVersionResponseSchema, @@ -16,12 +13,6 @@ import { type ApiV1SkillListResponse, type ApiV1SkillResponse, type ApiV1SkillResolveResponse, - type ApiV1SkillReportResponse, - type ApiV1SkillReportListResponse, - type ApiV1SkillReportTriageResponse, - type SkillReportFinalAction, - type SkillReportListStatus, - type SkillReportStatus, } from "../../schema/index.js"; import { extractZipToDir, @@ -33,7 +24,6 @@ import { writeLockfile, writeSkillOrigin, } from "../../skills.js"; -import { requireAuthToken } from "../authToken.js"; import { getRegistry } from "../registry.js"; import type { GlobalOpts, ResolveResult } from "../types.js"; import { @@ -45,33 +35,10 @@ import { selectAgent, selectScope, } from "../ui.js"; -import { presentModerationPlan, reportModerationPlan } from "./moderationPlan.js"; import { searchMultiselect, cancelSymbol } from "../prompts/search-multiselect.js"; import { getAgentLabel, resolveAgentWorkdir } from "../agents.js"; import type { AgentName } from "../agents.js"; -type SkillReportOptions = { - version?: string; - reason?: string; - json?: boolean; -}; - -type SkillReportListOptions = { - status?: SkillReportListStatus; - cursor?: string; - limit?: number; - json?: boolean; -}; - -type SkillReportTriageOptions = { - status?: SkillReportStatus; - action?: SkillReportFinalAction; - finalAction?: SkillReportFinalAction; - note?: string; - json?: boolean; - yes?: boolean; -}; - function normalizeSkillSlugOrFail(raw: string) { const slug = raw.trim(); if (!slug) fail("Slug required"); @@ -293,7 +260,10 @@ export async function cmdInstall( const subSpinner = createSpinner(`Downloading sub-skill ${subSlug}@${subVersion}`); try { - const zip = await downloadZip(registry, { slug: subSlug, version: subVersion }); + const zip = await downloadZip(registry, { + slug: subSlug, + version: subVersion, + }); await extractZipToDir(zip, subTarget); const installedFiles = await listTextFiles(subTarget); const installedFingerprint = @@ -361,7 +331,10 @@ export async function cmdInstall( } spinner.text = `Downloading ${trimmed}@${resolvedVersion}`; - const zip = await downloadZip(registry, { slug: trimmed, version: resolvedVersion }); + const zip = await downloadZip(registry, { + slug: trimmed, + version: resolvedVersion, + }); await extractZipToDir(zip, target); const installedFiles = await listTextFiles(target); const installedFingerprint = @@ -562,7 +535,10 @@ export async function cmdUpdate( spinner.start(`Updating ${entry} -> ${targetVersion}`); } await rm(target, { recursive: true, force: true }); - const zip = await downloadZip(registry, { slug: entry, version: targetVersion }); + const zip = await downloadZip(registry, { + slug: entry, + version: targetVersion, + }); await extractZipToDir(zip, target); const installedFiles = await listTextFiles(target); const installedFingerprint = @@ -812,134 +788,6 @@ export function clampLimit(limit: number, fallback = 25) { return Math.min(Math.max(1, limit), 200); } -export async function cmdReportSkill( - opts: GlobalOpts, - slug: string, - options: SkillReportOptions = {}, -) { - const trimmed = normalizeSkillSlugOrFail(slug); - const reason = options.reason?.trim(); - if (!reason) fail("--reason required"); - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/report`, - token, - body: { - reason, - ...(options.version?.trim() ? { version: options.version.trim() } : {}), - }, - }, - ApiV1SkillReportResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - if (result.alreadyReported) { - console.log(`Already reported ${trimmed}.`); - } else { - console.log(`OK. Reported ${trimmed} (${result.reportId}).`); - } -} - -export async function cmdListSkillReports(opts: GlobalOpts, options: SkillReportListOptions = {}) { - const status = options.status?.trim() || "open"; - if (!["open", "confirmed", "dismissed", "all"].includes(status)) { - fail("--status must be open, confirmed, dismissed, or all"); - } - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const url = registryUrl(`${ApiRoutes.skills}/-/reports`, registry); - url.searchParams.set("status", status); - if (options.cursor?.trim()) url.searchParams.set("cursor", options.cursor.trim()); - url.searchParams.set("limit", String(clampLimit(options.limit ?? 25, 25))); - const result = await apiRequest( - registry, - { method: "GET", url: url.toString(), token }, - ApiV1SkillReportListResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - if (result.items.length === 0) { - console.log("No skill reports found."); - } else { - for (const item of result.items) { - const reporter = item.reporter.handle ?? item.reporter.userId; - console.log(`${item.reportId} ${item.status} ${item.slug}`); - console.log(` reporter: ${reporter}`); - if (item.reason) console.log(` reason: ${item.reason}`); - if (item.triageNote) console.log(` note: ${item.triageNote}`); - } - } - if (!result.done && result.nextCursor) console.log(`Next cursor: ${result.nextCursor}`); -} - -export async function cmdTriageSkillReport( - opts: GlobalOpts, - reportId: string, - options: SkillReportTriageOptions = {}, -) { - const trimmed = reportId.trim(); - if (!trimmed) fail("Report id required"); - const statusValue = options.status?.trim(); - if (!statusValue || !["open", "confirmed", "dismissed"].includes(statusValue)) { - fail("--status must be open, confirmed, or dismissed"); - } - const status = statusValue as SkillReportStatus; - const finalAction = (options.finalAction ?? options.action)?.trim() as - | SkillReportFinalAction - | undefined; - if (finalAction && !["none", "hide"].includes(finalAction)) { - fail("--action must be none or hide"); - } - const note = options.note?.trim(); - if (status !== "open" && !note) fail("--note required unless reopening"); - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - await presentModerationPlan( - reportModerationPlan({ - entityLabel: "skill", - reportId: trimmed, - status, - finalAction: finalAction ?? "none", - }), - options, - ); - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/-/reports/${encodeURIComponent(trimmed)}/triage`, - token, - body: { - status, - ...(note ? { note } : {}), - ...(finalAction ? { finalAction } : {}), - }, - }, - ApiV1SkillReportTriageResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - const actionSuffix = - result.actionTaken && result.actionTaken !== "none" ? `; action ${result.actionTaken}` : ""; - console.log(`OK. Skill report ${trimmed} set to ${result.status}${actionSuffix}.`); -} - function formatRelativeTime(timestamp: number): string { const now = Date.now(); const diff = now - timestamp; diff --git a/dt-skill/src/cli/commands/sync.test.ts b/dt-skill/src/cli/commands/sync.test.ts index 14aac957..426e806d 100644 --- a/dt-skill/src/cli/commands/sync.test.ts +++ b/dt-skill/src/cli/commands/sync.test.ts @@ -1,9 +1,7 @@ /* @vitest-environment node */ import { afterEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, +import { createHttpModuleMocks, createRegistryModuleMocks, createUiModuleMocks, makeGlobalOpts, @@ -34,7 +32,6 @@ vi.mock("@clack/prompts", () => ({ isCancel: () => false, })); -const authTokenMocks = createAuthTokenModuleMocks(); const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks(); @@ -44,7 +41,6 @@ httpMocks.downloadZip.mockImplementation( const mockApiRequest = httpMocks.apiRequest; const mockFail = uiMocks.fail; const mockSpinner = uiMocks.spinner; -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); vi.mock("../registry.js", () => registryMocks.moduleFactory()); vi.mock("../../http.js", () => httpMocks.moduleFactory()); vi.mock("../ui.js", () => ({ @@ -121,7 +117,6 @@ describe("cmdSync", () => { it("classifies skills as new/update/synced (dry-run, mocked HTTP)", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -158,7 +153,6 @@ describe("cmdSync", () => { return initialValues; }); mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -194,7 +188,6 @@ describe("cmdSync", () => { it("labels unmatched local content as proposed publish versions, not registry updates", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -223,7 +216,6 @@ describe("cmdSync", () => { it("shows condensed synced list when nothing to sync", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { return { match: { version: "1.0.0" }, latestVersion: { version: "1.0.0" } }; @@ -255,7 +247,6 @@ describe("cmdSync", () => { }); mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { return { match: null, latestVersion: null }; @@ -285,7 +276,6 @@ describe("cmdSync", () => { return []; }); mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { throw new Error("Skill not found"); @@ -303,7 +293,6 @@ describe("cmdSync", () => { it("allows empty changelog for updates (interactive)", async () => { interactive = true; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -334,7 +323,6 @@ describe("cmdSync", () => { it("continues uploading after a publish failure", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -379,7 +367,6 @@ describe("cmdSync", () => { it("continues uploading after an alias slug conflict publish failure", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -426,7 +413,6 @@ describe("cmdSync", () => { it("continues uploading after a locked slug publish failure", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -472,7 +458,6 @@ describe("cmdSync", () => { it("records unrelated publish failures as per-skill failures", async () => { interactive = false; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { const u = new URL(`https://x.test${args.path}`); @@ -516,7 +501,6 @@ describe("cmdSync", () => { return [{ folder: "/scan/update-skill", slug: "update-skill", displayName: "Update Skill" }]; }); mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path === "/api/cli/telemetry/sync") return { ok: true }; if (args.path.startsWith("/api/v1/resolve?")) { return { match: null, latestVersion: { version: "1.0.0" } }; @@ -539,7 +523,6 @@ describe("cmdSync", () => { interactive = false; process.env.CLAWHUB_DISABLE_TELEMETRY = "1"; mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/v1/whoami") return { user: { handle: "steipete" } }; if (args.path.startsWith("/api/v1/resolve?")) { return { match: { version: "1.0.0" }, latestVersion: { version: "1.0.0" } }; } diff --git a/dt-skill/src/cli/commands/syncHelpers.ts b/dt-skill/src/cli/commands/syncHelpers.ts index e48ed3a2..635bc6a1 100644 --- a/dt-skill/src/cli/commands/syncHelpers.ts +++ b/dt-skill/src/cli/commands/syncHelpers.ts @@ -4,7 +4,7 @@ import { resolve } from "node:path"; import { isCancel, multiselect } from "@clack/prompts"; import semver from "semver"; import { resolveHome } from "../../homedir.js"; -import { apiRequest, downloadZip } from "../../http.js"; +import { apiRequest, downloadZip, registryUrl } from "../../http.js"; import { ApiCliTelemetrySyncResponseSchema, ApiRoutes, @@ -160,7 +160,10 @@ export async function checkRegistrySyncState( }; } - const zip = await downloadZip(registry, { slug: skill.slug, version: latestVersion }); + const zip = await downloadZip(registry, { + slug: skill.slug, + version: latestVersion, + }); const remote = hashSkillZip(zip).fingerprint; const matchVersion = remote === skill.fingerprint ? latestVersion : null; diff --git a/dt-skill/src/cli/commands/transfer.test.ts b/dt-skill/src/cli/commands/transfer.test.ts deleted file mode 100644 index 00d5fc43..00000000 --- a/dt-skill/src/cli/commands/transfer.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* @vitest-environment node */ - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTokenModuleMocks, - createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; - -const authTokenMocks = createAuthTokenModuleMocks(); -const registryMocks = createRegistryModuleMocks(); -const httpMocks = createHttpModuleMocks(); -const uiMocks = createUiModuleMocks(); - -vi.mock("../authToken.js", () => authTokenMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../ui.js", () => uiMocks.moduleFactory()); - -const { - cmdTransferAccept, - cmdTransferCancel, - cmdTransferList, - cmdTransferReject, - cmdTransferRequest, -} = await import("./transfer"); - -const consoleLog = vi.spyOn(console, "log").mockImplementation(() => {}); - -afterEach(() => { - vi.clearAllMocks(); -}); - -describe("transfer commands", () => { - it("request requires --yes when input is disabled", async () => { - await expect(cmdTransferRequest(makeGlobalOpts(), "demo", "@alice", {}, false)).rejects.toThrow( - /--yes/i, - ); - }); - - it("request calls transfer endpoint", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - transferId: "skillOwnershipTransfers:1", - toUserHandle: "alice", - expiresAt: Date.now() + 10_000, - }); - - await cmdTransferRequest( - makeGlobalOpts(), - "Demo", - "@Alice", - { yes: true, message: "Please take over" }, - false, - ); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "POST", - path: "/api/v1/skills/demo/transfer", - }), - expect.anything(), - ); - const requestArgs = httpMocks.apiRequest.mock.calls[0]?.[1] as { body?: unknown }; - expect(requestArgs.body).toEqual({ - toUserHandle: "alice", - message: "Please take over", - }); - }); - - it("list calls incoming transfers endpoint", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - transfers: [], - }); - await cmdTransferList(makeGlobalOpts(), {}); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "GET", - path: "/api/v1/transfers/incoming", - }), - expect.anything(), - ); - expect(consoleLog).toHaveBeenCalledWith("No incoming transfers."); - }); - - it("list supports outgoing endpoint", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - transfers: [], - }); - await cmdTransferList(makeGlobalOpts(), { outgoing: true }); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "GET", - path: "/api/v1/transfers/outgoing", - }), - expect.anything(), - ); - expect(consoleLog).toHaveBeenCalledWith("No outgoing transfers."); - }); - - it("accept/reject/cancel call action endpoints", async () => { - httpMocks.apiRequest.mockResolvedValue({ - ok: true, - skillSlug: "demo", - }); - - await cmdTransferAccept(makeGlobalOpts(), "demo", { yes: true }, false); - await cmdTransferReject(makeGlobalOpts(), "demo", { yes: true }, false); - await cmdTransferCancel(makeGlobalOpts(), "demo", { yes: true }, false); - - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/transfer/accept" }), - expect.anything(), - ); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/transfer/reject" }), - expect.anything(), - ); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/transfer/cancel" }), - expect.anything(), - ); - }); -}); diff --git a/dt-skill/src/cli/commands/transfer.ts b/dt-skill/src/cli/commands/transfer.ts deleted file mode 100644 index b5a18f94..00000000 --- a/dt-skill/src/cli/commands/transfer.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { apiRequest } from "../../http.js"; -import { - ApiRoutes, - ApiV1TransferDecisionResponseSchema, - ApiV1TransferListResponseSchema, - ApiV1TransferRequestResponseSchema, - parseArk, -} from "../../schema/index.js"; -import { requireAuthToken } from "../authToken.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; - -type ConfirmOptions = { yes?: boolean }; - -type DecisionAction = "accept" | "reject" | "cancel"; - -type DecisionSpec = { - verb: string; - progress: string; - success: string; - action: DecisionAction; -}; - -const DECISION_SPECS: Record = { - accept: { - verb: "Accept", - progress: "Accepting", - success: "Transfer accepted", - action: "accept", - }, - reject: { - verb: "Reject", - progress: "Rejecting", - success: "Transfer rejected", - action: "reject", - }, - cancel: { - verb: "Cancel", - progress: "Cancelling", - success: "Transfer cancelled", - action: "cancel", - }, -}; - -function normalizeSlug(slugArg: string) { - const slug = slugArg.trim().toLowerCase(); - if (!slug) fail("Skill slug required"); - return slug; -} - -function canPrompt(inputAllowed: boolean) { - return isInteractive() && inputAllowed !== false; -} - -async function requireYesOrConfirm(options: ConfirmOptions, inputAllowed: boolean, prompt: string) { - if (options.yes) return true; - if (!canPrompt(inputAllowed)) fail("Pass --yes (no input)"); - return promptConfirm(prompt); -} - -export async function cmdTransferRequest( - opts: GlobalOpts, - slugArg: string, - toHandleArg: string, - options: ConfirmOptions & { message?: string }, - inputAllowed: boolean, -) { - const slug = normalizeSlug(slugArg); - const toHandle = toHandleArg.trim().replace(/^@+/, "").toLowerCase(); - if (!toHandle) fail("Recipient handle required (e.g., @username)"); - - const confirmed = await requireYesOrConfirm( - options, - inputAllowed, - `Transfer ${slug} to @${toHandle}? User transfers require recipient acceptance; org transfers apply immediately if you are an admin of both owners.`, - ); - if (!confirmed) return undefined; - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Requesting transfer of ${slug} to @${toHandle}`); - - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/transfer`, - token, - body: { - toUserHandle: toHandle, - message: options.message, - }, - }, - ApiV1TransferRequestResponseSchema, - ); - const parsed = parseArk( - ApiV1TransferRequestResponseSchema, - result, - "Transfer request response", - ); - if (parsed.transferred) { - spinner.succeed(`Transferred ${slug} to @${parsed.toPublisherHandle ?? toHandle}`); - } else { - spinner.succeed(`Transfer requested for ${slug} to @${parsed.toUserHandle ?? toHandle}`); - } - return parsed; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdTransferList(opts: GlobalOpts, options: { outgoing?: boolean }) { - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Fetching transfers"); - - try { - const path = options.outgoing - ? `${ApiRoutes.transfers}/outgoing` - : `${ApiRoutes.transfers}/incoming`; - const result = await apiRequest( - registry, - { method: "GET", path, token }, - ApiV1TransferListResponseSchema, - ); - const parsed = parseArk(ApiV1TransferListResponseSchema, result, "Transfer list response"); - spinner.stop(); - - if (parsed.transfers.length === 0) { - console.log(options.outgoing ? "No outgoing transfers." : "No incoming transfers."); - return parsed; - } - - console.log(options.outgoing ? "Outgoing transfers:" : "Incoming transfers:"); - for (const transfer of parsed.transfers) { - const otherHandle = options.outgoing ? transfer.toUser?.handle : transfer.fromUser?.handle; - const other = otherHandle ? `@${otherHandle.replace(/^@+/, "")}` : "(unknown user)"; - const expiresInDays = Math.max( - 0, - Math.ceil((transfer.expiresAt - Date.now()) / (24 * 60 * 60 * 1000)), - ); - console.log(` ${transfer.skill.slug} -> ${other} (expires in ${expiresInDays}d)`); - } - return parsed; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -async function runTransferDecision( - opts: GlobalOpts, - slugArg: string, - options: ConfirmOptions, - inputAllowed: boolean, - spec: DecisionSpec, -) { - const slug = normalizeSlug(slugArg); - const confirmed = await requireYesOrConfirm( - options, - inputAllowed, - `${spec.verb} transfer of ${slug}?`, - ); - if (!confirmed) return undefined; - - const token = await requireAuthToken(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`${spec.progress} transfer of ${slug}`); - - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/transfer/${spec.action}`, - token, - }, - ApiV1TransferDecisionResponseSchema, - ); - const parsed = parseArk(ApiV1TransferDecisionResponseSchema, result, "Transfer response"); - spinner.succeed(`${spec.success} (${slug})`); - return parsed; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export function cmdTransferAccept( - opts: GlobalOpts, - slugArg: string, - options: ConfirmOptions, - inputAllowed: boolean, -) { - return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.accept); -} - -export function cmdTransferReject( - opts: GlobalOpts, - slugArg: string, - options: ConfirmOptions, - inputAllowed: boolean, -) { - return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.reject); -} - -export function cmdTransferCancel( - opts: GlobalOpts, - slugArg: string, - options: ConfirmOptions, - inputAllowed: boolean, -) { - return runTransferDecision(opts, slugArg, options, inputAllowed, DECISION_SPECS.cancel); -} diff --git a/dt-skill/src/cli/registry.test.ts b/dt-skill/src/cli/registry.test.ts index 10694c69..3760767d 100644 --- a/dt-skill/src/cli/registry.test.ts +++ b/dt-skill/src/cli/registry.test.ts @@ -54,7 +54,7 @@ describe("registry resolution", () => { }); it("ignores legacy registry and updates cache from discovery", async () => { - readGlobalConfig.mockResolvedValue({ registry: "https://auth.clawdhub.com", token: "tkn" }); + readGlobalConfig.mockResolvedValue({ registry: "https://auth.clawdhub.com" }); discoverRegistryFromSite.mockResolvedValue({ apiBase: "http://10.0.0.8:7001" }); const registry = await getRegistry(makeOpts({ site: "http://10.0.0.8:7001" }), { cache: true }); @@ -62,12 +62,11 @@ describe("registry resolution", () => { expect(registry).toBe("http://10.0.0.8:7001"); expect(writeGlobalConfig).toHaveBeenCalledWith({ registry: "http://10.0.0.8:7001", - token: "tkn", }); }); it("fails clearly when no explicit, cached, or discoverable registry exists", async () => { - readGlobalConfig.mockResolvedValue({ registry: "https://registry.clawhub.ai", token: "tkn" }); + readGlobalConfig.mockResolvedValue({ registry: "https://registry.clawhub.ai" }); discoverRegistryFromSite.mockResolvedValue(null); await expect(getRegistry(makeOpts(), { cache: true })).rejects.toThrow( @@ -87,7 +86,6 @@ describe("registry resolution", () => { expect(registry).toBe("http://10.0.0.8:7001"); expect(writeGlobalConfig).toHaveBeenCalledWith({ registry: "http://10.0.0.8:7001", - token: undefined, }); }); }); diff --git a/dt-skill/src/cli/registry.ts b/dt-skill/src/cli/registry.ts index 4341ee71..31f6e289 100644 --- a/dt-skill/src/cli/registry.ts +++ b/dt-skill/src/cli/registry.ts @@ -41,7 +41,7 @@ export async function getRegistry(opts: GlobalOpts, params?: { cache?: boolean } !cached || isLegacyRegistry(cached) || cached !== registry; - if (shouldUpdate) await writeGlobalConfig({ registry, token: cfg?.token }); + if (shouldUpdate) await writeGlobalConfig({ registry }); return registry; } diff --git a/dt-skill/src/config.test.ts b/dt-skill/src/config.test.ts index 6eeb1992..fed456dc 100644 --- a/dt-skill/src/config.test.ts +++ b/dt-skill/src/config.test.ts @@ -56,7 +56,7 @@ afterEach(() => { describe("writeGlobalConfig", () => { it("writes config with restricted modes", async () => { - await writeGlobalConfig({ registry: "https://example.com", token: "clh_test" }); + await writeGlobalConfig({ registry: "https://example.com" }); expect(fsMocks.mkdir).toHaveBeenCalledWith("/tmp/clawhub-config-test", { recursive: true, @@ -64,7 +64,7 @@ describe("writeGlobalConfig", () => { }); expect(fsMocks.writeFile).toHaveBeenCalledWith( testConfigPath, - expect.stringContaining('"token": "clh_test"'), + expect.stringContaining('"registry": "https://example.com"'), { encoding: "utf8", mode: 0o600, diff --git a/dt-skill/src/config.ts b/dt-skill/src/config.ts index 1bc5264a..e114111a 100644 --- a/dt-skill/src/config.ts +++ b/dt-skill/src/config.ts @@ -66,7 +66,6 @@ export async function writeGlobalConfig(config: GlobalConfig) { await mkdir(dir, { recursive: true, mode: 0o700 }); // Write file with restricted permissions (owner read/write only) - // This protects API tokens from being read by other users await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600, diff --git a/dt-skill/src/deviceAuth.test.ts b/dt-skill/src/deviceAuth.test.ts deleted file mode 100644 index ff3f5a51..00000000 --- a/dt-skill/src/deviceAuth.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { pollForDeviceToken, requestDeviceCode } from "./deviceAuth.js"; - -describe("deviceAuth", () => { - describe("requestDeviceCode", () => { - it("should POST to /api/cli/device/code and return device code response", async () => { - const mockResponse = { - device_code: "abc123", - user_code: "ABCD-1234", - verification_uri: "https://clawhub.ai/device", - expires_in: 900, - interval: 5, - }; - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve(mockResponse), - }); - - const result = await requestDeviceCode({ - apiUrl: "https://api.example", - siteUrl: "https://clawhub.ai", - }); - - expect(result).toEqual(mockResponse); - expect(global.fetch).toHaveBeenCalledWith( - "https://api.example/api/cli/device/code", - expect.objectContaining({ - method: "POST", - headers: expect.objectContaining({ - "Content-Type": "application/json", - }), - }), - ); - }); - - it("should throw on non-ok response", async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 404, - statusText: "Not Found", - text: () => Promise.resolve("endpoint not found"), - }); - - await expect( - requestDeviceCode({ apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }), - ).rejects.toThrow("Device code request failed (404)"); - }); - - it("should throw on invalid response (missing fields)", async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ device_code: "abc" }), - }); - - await expect( - requestDeviceCode({ apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }), - ).rejects.toThrow("Invalid device code response"); - }); - }); - - describe("pollForDeviceToken", () => { - it("should return token on successful authorization", async () => { - const tokenResponse = { - access_token: "token123", - token_type: "bearer", - scope: "read write", - }; - - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve(tokenResponse), - }); - - const result = await pollForDeviceToken( - { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, - "device_code_123", - { interval: 0.01, expiresIn: 10 }, - ); - - expect(result.access_token).toBe("token123"); - }); - - it("should keep polling on authorization_pending", async () => { - let callCount = 0; - global.fetch = vi.fn().mockImplementation(() => { - callCount++; - if (callCount < 3) { - return Promise.resolve({ - ok: false, - json: () => Promise.resolve({ error: "authorization_pending" }), - }); - } - return Promise.resolve({ - ok: true, - json: () => - Promise.resolve({ - access_token: "token_after_wait", - token_type: "bearer", - scope: "read write", - }), - }); - }); - - const result = await pollForDeviceToken( - { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, - "device_code_123", - { interval: 0.01, expiresIn: 10 }, - ); - - expect(result.access_token).toBe("token_after_wait"); - expect(callCount).toBe(3); - }); - - it("should throw on access_denied", async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - json: () => Promise.resolve({ error: "access_denied" }), - }); - - await expect( - pollForDeviceToken( - { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, - "device_code_123", - { - interval: 0.01, - expiresIn: 10, - }, - ), - ).rejects.toThrow("Authorization denied"); - }); - - it("should throw on expired_token", async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - json: () => Promise.resolve({ error: "expired_token" }), - }); - - await expect( - pollForDeviceToken( - { apiUrl: "https://api.example", siteUrl: "https://clawhub.ai" }, - "device_code_123", - { - interval: 0.01, - expiresIn: 10, - }, - ), - ).rejects.toThrow("expired"); - }); - }); -}); diff --git a/dt-skill/src/deviceAuth.ts b/dt-skill/src/deviceAuth.ts deleted file mode 100644 index 207f2a17..00000000 --- a/dt-skill/src/deviceAuth.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * GitHub Device Flow authentication for headless environments. - * - * Implements RFC 8628 / GitHub's Device Flow: - * https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow - * - * This allows CLI authentication without a browser redirect to localhost, - * enabling headless agents and remote servers to authenticate. - */ - -type DeviceCodeResponse = { - device_code: string; - user_code: string; - verification_uri: string; - expires_in: number; - interval: number; -}; - -type DeviceTokenResponse = { - access_token: string; - token_type: string; - scope: string; -}; - -type DeviceTokenErrorResponse = { - error: string; - error_description?: string; - interval?: number; -}; - -type DeviceFlowConfig = { - /** The ClawHub API URL that exposes device flow endpoints */ - apiUrl: string; - /** The ClawHub site URL that hosts the verification page */ - siteUrl: string; - /** Client ID for the OAuth app (provided by ClawHub) */ - clientId?: string; - /** Scope to request */ - scope?: string; -}; - -const DEFAULT_SCOPE = "read write"; - -/** - * Request a device code from the ClawHub device flow endpoint. - */ -export async function requestDeviceCode(config: DeviceFlowConfig): Promise { - const url = new URL("/api/cli/device/code", config.apiUrl); - - const body: Record = { - scope: config.scope ?? DEFAULT_SCOPE, - site_url: config.siteUrl, - }; - if (config.clientId) { - body.client_id = config.clientId; - } - - const response = await fetch(url.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const text = await response.text().catch(() => ""); - throw new Error( - `Device code request failed (${response.status}): ${text || response.statusText}`, - ); - } - - const data = (await response.json()) as DeviceCodeResponse; - - if (!data.device_code || !data.user_code || !data.verification_uri) { - throw new Error("Invalid device code response from server"); - } - - return data; -} - -/** - * Poll for the device flow token until the user completes authorization, - * the code expires, or an unrecoverable error occurs. - */ -export async function pollForDeviceToken( - config: DeviceFlowConfig, - deviceCode: string, - options: { interval: number; expiresIn: number }, -): Promise { - const url = new URL("/api/cli/device/token", config.apiUrl); - const deadline = Date.now() + options.expiresIn * 1000; - let interval = options.interval * 1000; - - const body: Record = { - device_code: deviceCode, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }; - if (config.clientId) { - body.client_id = config.clientId; - } - - while (Date.now() < deadline) { - await sleep(interval); - - const response = await fetch(url.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(body), - }); - - // Parse JSON once to avoid "body already read" errors - const data = (await response.json().catch(() => ({}))) as - | DeviceTokenResponse - | DeviceTokenErrorResponse; - - if (response.ok && "access_token" in data && data.access_token) { - return data as DeviceTokenResponse; - } - - const errorData = data as DeviceTokenErrorResponse; - - switch (errorData.error) { - case "authorization_pending": - // User hasn't completed auth yet — keep polling - break; - case "slow_down": - // Server requests longer interval - interval = (errorData.interval ?? Math.ceil(interval / 1000) + 5) * 1000; - break; - case "expired_token": - throw new Error("Device code expired. Please try again."); - case "access_denied": - throw new Error("Authorization denied by user."); - default: - throw new Error( - `Device flow error: ${errorData.error_description || errorData.error || "unknown error"}`, - ); - } - } - - throw new Error("Device code expired (timeout). Please try again."); -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/dt-skill/src/discovery.test.ts b/dt-skill/src/discovery.test.ts index 4e26b4ac..c9fa9c26 100644 --- a/dt-skill/src/discovery.test.ts +++ b/dt-skill/src/discovery.test.ts @@ -34,7 +34,6 @@ describe("discovery", () => { ); await expect(discoverRegistryFromSite("https://example.com")).resolves.toEqual({ apiBase: "https://example.convex.site", - authBase: undefined, minCliVersion: undefined, }); }); @@ -47,7 +46,6 @@ describe("discovery", () => { new Response( JSON.stringify({ apiBase: "https://api.example.com", - authBase: "https://auth.example.com", minCliVersion: "1.2.3", }), { @@ -59,7 +57,6 @@ describe("discovery", () => { ); await expect(discoverRegistryFromSite("https://example.com")).resolves.toEqual({ apiBase: "https://api.example.com", - authBase: "https://auth.example.com", minCliVersion: "1.2.3", }); }); diff --git a/dt-skill/src/discovery.ts b/dt-skill/src/discovery.ts index 1a9964eb..b7807ac3 100644 --- a/dt-skill/src/discovery.ts +++ b/dt-skill/src/discovery.ts @@ -15,7 +15,6 @@ export async function discoverRegistryFromSite(siteUrl: string) { if (!apiBase) return null; return { apiBase, - authBase: parsed.authBase, minCliVersion: parsed.minCliVersion, }; } diff --git a/dt-skill/src/homedir.ts b/dt-skill/src/homedir.ts index 92f81e7e..eea260d7 100644 --- a/dt-skill/src/homedir.ts +++ b/dt-skill/src/homedir.ts @@ -5,7 +5,7 @@ import { win32 } from "node:path"; * Resolve the user's home directory, preferring environment variables over * os.homedir(). On Linux, os.homedir() reads from /etc/passwd which can * return a stale path after a user rename (usermod -l). The $HOME env var - * is set by the login process and reflects the current session. + * is resolved from the current runtime environment. */ export function resolveHome(): string { if (process.platform === "win32") { diff --git a/dt-skill/src/http.bun.test.ts b/dt-skill/src/http.bun.test.ts index 48b15f03..dbe00158 100644 --- a/dt-skill/src/http.bun.test.ts +++ b/dt-skill/src/http.bun.test.ts @@ -63,7 +63,6 @@ describe("bun http client", () => { const getResult = await client.apiRequest<{ ok: boolean }>("https://registry.example", { method: "GET", path: "/v1/ping", - token: "clh_token", }); await client.apiRequest("https://registry.example", { method: "POST", @@ -75,7 +74,7 @@ describe("bun http client", () => { const [, getArgs] = spawnImpl.mock.calls[0] as [string, string[]]; expect(getArgs).toContain("GET"); expect(getArgs).toContain("https://registry.example/v1/ping"); - expect(getArgs).toContain("Authorization: Bearer clh_token"); + expect(getArgs.some((arg) => arg.startsWith("Authorization: Bearer"))).toBe(false); const [, postArgs] = spawnImpl.mock.calls[1] as [string, string[]]; expect(postArgs).toContain("Content-Type: application/json"); @@ -142,11 +141,10 @@ describe("bun http client", () => { ).resolves.toBe("hello world"); const bytes = await client.downloadZip("https://registry.example", { slug: "demo", - token: "t", }); expect(Array.from(bytes)).toEqual(Array.from(Buffer.from("not found"))); await expect( - client.downloadZip("https://registry.example", { slug: "demo", token: "t" }), + client.downloadZip("https://registry.example", { slug: "demo" }), ).rejects.toThrow("not found"); expect(readFileImpl).toHaveBeenCalled(); diff --git a/dt-skill/src/http.test.ts b/dt-skill/src/http.test.ts index c3923087..1446ea04 100644 --- a/dt-skill/src/http.test.ts +++ b/dt-skill/src/http.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it, vi } from "vitest"; import { createHttpClient, detectHttpRuntime, registryUrl, shouldUseProxyFromEnv } from "./http.js"; -import { ApiV1WhoamiResponseSchema } from "./schema/index.js"; function createNodeClient(options?: { fetchImpl?: typeof fetch; @@ -102,22 +101,21 @@ describe("registryUrl", () => { }); describe("node http client", () => { - it("adds bearer token and parses json", async () => { + it("parses json without adding authorization", async () => { const fetchImpl = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ user: { handle: null } }), }); const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); - const result = await client.apiRequest( - "https://example.com", - { method: "GET", path: "/x", token: "clh_token" }, - ApiV1WhoamiResponseSchema, - ); + const result = await client.apiRequest<{ user: { handle: null } }>("https://example.com", { + method: "GET", + path: "/x", + }); expect(result.user.handle).toBeNull(); const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; - expect((init.headers as Record).Authorization).toBe("Bearer clh_token"); + expect((init.headers as Record).Authorization).toBeUndefined(); }); it("posts json body", async () => { @@ -289,7 +287,7 @@ describe("node http client", () => { await expect( client.apiRequest("https://example.com", { method: "GET", path: "/auth" }), - ).rejects.toThrow(/clawhub login.*deleted, banned, or disabled/i); + ).rejects.toThrow(/authentication required/i); await expect( client.apiRequest("https://example.com", { method: "GET", path: "/forbidden" }), ).rejects.toThrow(/account does not have access.*not in good standing/i); @@ -315,7 +313,6 @@ describe("node http client", () => { const bytes = await client.downloadZip("https://example.com", { slug: "demo", version: "1.0.0", - token: "clh_token", }); expect(Array.from(bytes)).toEqual([1, 2, 3]); @@ -366,13 +363,12 @@ describe("node http client", () => { const result = await successClient.apiRequestForm("https://example.com", { method: "POST", path: "/upload", - token: "clh_token", form, }); expect(result).toEqual({ ok: true }); const [, init] = successFetch.mock.calls[0] as [string, RequestInit]; expect(init.body).toBe(form); - expect((init.headers as Record).Authorization).toBe("Bearer clh_token"); + expect((init.headers as Record).Authorization).toBeUndefined(); const rateLimitedFetch = vi.fn().mockResolvedValue({ ok: false, diff --git a/dt-skill/src/http.ts b/dt-skill/src/http.ts index f5a21dbb..9820f10e 100644 --- a/dt-skill/src/http.ts +++ b/dt-skill/src/http.ts @@ -35,23 +35,21 @@ type RequestArgs = | { method: "GET" | "POST" | "DELETE"; path: string; - token?: string; body?: unknown; retryCount?: number; } | { method: "GET" | "POST" | "DELETE"; url: string; - token?: string; body?: unknown; retryCount?: number; }; type FormRequestArgs = - | { method: "POST"; path: string; token?: string; form: FormData; retryCount?: number } - | { method: "POST"; url: string; token?: string; form: FormData; retryCount?: number }; + | { method: "POST"; path: string; form: FormData; retryCount?: number } + | { method: "POST"; url: string; form: FormData; retryCount?: number }; -type TextRequestArgs = { path: string; token?: string } | { url: string; token?: string }; +type TextRequestArgs = { path: string } | { url: string }; type HeaderSource = Headers | Record | null | undefined; @@ -93,7 +91,7 @@ type HttpClient = { fetchBinary(registry: string, args: TextRequestArgs): Promise; downloadZip( registry: string, - args: { slug: string; version?: string; token?: string }, + args: { slug: string; version?: string }, ): Promise; }; @@ -162,7 +160,6 @@ export function createHttpClient(options: HttpClientOptions = {}): HttpClient { } const headers: Record = { Accept: "application/json" }; - if (args.token) headers.Authorization = `Bearer ${args.token}`; let body: string | undefined; if (args.body !== undefined || args.method === "POST") { headers["Content-Type"] = "application/json"; @@ -199,7 +196,6 @@ export function createHttpClient(options: HttpClientOptions = {}): HttpClient { } const headers: Record = { Accept: "application/json" }; - if (args.token) headers.Authorization = `Bearer ${args.token}`; const response = await fetchWithTimeout( deps, url, @@ -228,11 +224,10 @@ export function createHttpClient(options: HttpClientOptions = {}): HttpClient { const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); return await runWithRetries(async () => { if (deps.runtime === "bun") { - return await fetchTextViaCurl(deps, url, args); + return await fetchTextViaCurl(deps, url); } const headers: Record = { Accept: "text/plain" }; - if (args.token) headers.Authorization = `Bearer ${args.token}`; const response = await fetchWithTimeout(deps, url, { method: "GET", headers }); const text = await response.text(); if (!response.ok) { @@ -246,11 +241,10 @@ export function createHttpClient(options: HttpClientOptions = {}): HttpClient { const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); return await runWithRetries(async () => { if (deps.runtime === "bun") { - return await fetchBinaryViaCurl(deps, url, args.token); + return await fetchBinaryViaCurl(deps, url); } const headers: Record = {}; - if (args.token) headers.Authorization = `Bearer ${args.token}`; const response = await fetchWithTimeout(deps, url, { method: "GET", headers }); if (!response.ok) { throwHttpStatusError( @@ -266,18 +260,17 @@ export function createHttpClient(options: HttpClientOptions = {}): HttpClient { async function downloadZipRequest( registry: string, - args: { slug: string; version?: string; token?: string }, + args: { slug: string; version?: string }, ) { const url = registryUrl(ApiRoutes.download, registry); url.searchParams.set("slug", args.slug); if (args.version) url.searchParams.set("version", args.version); return await runWithRetries(async () => { if (deps.runtime === "bun") { - return await fetchBinaryViaCurl(deps, url.toString(), args.token); + return await fetchBinaryViaCurl(deps, url.toString()); } const headers: Record = {}; - if (args.token) headers.Authorization = `Bearer ${args.token}`; const response = await fetchWithTimeout(deps, url.toString(), { method: "GET", headers }); if (!response.ok) { throwHttpStatusError( @@ -363,7 +356,7 @@ export async function fetchBinary(registry: string, args: TextRequestArgs): Prom export async function downloadZip( registry: string, - args: { slug: string; version?: string; token?: string }, + args: { slug: string; version?: string }, ) { return await defaultHttpClient.downloadZip(registry, args); } @@ -490,7 +483,7 @@ function normalizeHttpErrorBody(status: number, text: string): string { return body; } if (status === 401) { - return "Authentication failed. Run `clawhub login` again. Deleted, banned, or disabled ClawHub accounts cannot use API tokens."; + return "Authentication required for this operation."; } if (status === 403) { return "Permission denied. This account does not have access to this operation, or the account is not in good standing."; @@ -588,7 +581,6 @@ async function fetchJsonViaCurl( args: RequestArgs, ) { const headers = ["-H", "Accept: application/json"]; - if (args.token) headers.push("-H", `Authorization: Bearer ${args.token}`); const curlArgs = [ "--silent", "--show-error", @@ -633,7 +625,6 @@ async function fetchJsonFormViaCurl( args: FormRequestArgs, ) { const headers = ["-H", "Accept: application/json"]; - if (args.token) headers.push("-H", `Authorization: Bearer ${args.token}`); const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "clawhub-upload-")); try { @@ -683,10 +674,8 @@ async function fetchJsonFormViaCurl( async function fetchTextViaCurl( deps: Pick, url: string, - args: { token?: string }, ) { const headers = ["-H", "Accept: text/plain"]; - if (args.token) headers.push("-H", `Authorization: Bearer ${args.token}`); const curlArgs = [ "--silent", "--show-error", @@ -717,13 +706,11 @@ async function fetchBinaryViaCurl( "spawnSyncImpl" | "mkdtempImpl" | "readFileImpl" | "rmImpl" | "tmpdirPath" | "now" >, url: string, - token?: string, ) { const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "clawhub-download-")); const filePath = join(tempDir, "payload.bin"); try { const headers: string[] = []; - if (token) headers.push("-H", `Authorization: Bearer ${token}`); const curlArgs = [ "--silent", diff --git a/dt-skill/src/schema/packages.ts b/dt-skill/src/schema/packages.ts index b63cdcbb..97611840 100644 --- a/dt-skill/src/schema/packages.ts +++ b/dt-skill/src/schema/packages.ts @@ -736,16 +736,3 @@ export const ApiV1PackageTrustedPublisherResponseSchema = type({ }); export type ApiV1PackageTrustedPublisherResponse = (typeof ApiV1PackageTrustedPublisherResponseSchema)[inferred]; - -export const PublishTokenMintRequestSchema = type({ - packageName: "string", - version: "string", - githubOidcToken: "string", -}); -export type PublishTokenMintRequest = (typeof PublishTokenMintRequestSchema)[inferred]; - -export const ApiV1PublishTokenMintResponseSchema = type({ - token: "string", - expiresAt: "number", -}); -export type ApiV1PublishTokenMintResponse = (typeof ApiV1PublishTokenMintResponseSchema)[inferred]; diff --git a/dt-skill/src/schema/routes.ts b/dt-skill/src/schema/routes.ts index fcc40f36..74e5d81d 100644 --- a/dt-skill/src/schema/routes.ts +++ b/dt-skill/src/schema/routes.ts @@ -3,7 +3,6 @@ export const LegacyApiRoutes = { search: "/api/search", skill: "/api/skill", skillResolve: "/api/skill/resolve", - cliWhoami: "/api/cli/whoami", cliUploadUrl: "/api/cli/upload-url", cliPublish: "/api/cli/publish", cliTelemetrySync: "/api/cli/telemetry/sync", @@ -15,7 +14,6 @@ export const ApiRoutes = { search: "/api/v1/search", resolve: "/api/v1/resolve", download: "/api/v1/download", - publishTokenMint: "/api/v1/publish/token/mint", skills: "/api/v1/skills", packages: "/api/v1/packages", codePlugins: "/api/v1/code-plugins", @@ -25,5 +23,4 @@ export const ApiRoutes = { publishers: "/api/v1/publishers", souls: "/api/v1/souls", users: "/api/v1/users", - whoami: "/api/v1/whoami", } as const; diff --git a/dt-skill/src/schema/schemas.ts b/dt-skill/src/schema/schemas.ts index 29282c4d..6513c591 100644 --- a/dt-skill/src/schema/schemas.ts +++ b/dt-skill/src/schema/schemas.ts @@ -2,17 +2,14 @@ import { type inferred, type } from "arktype"; export const GlobalConfigSchema = type({ registry: "string", - token: "string?", }); export type GlobalConfig = (typeof GlobalConfigSchema)[inferred]; export const WellKnownConfigSchema = type({ apiBase: "string", - authBase: "string?", minCliVersion: "string?", }).or({ registry: "string", - authBase: "string?", minCliVersion: "string?", }); export type WellKnownConfig = (typeof WellKnownConfigSchema)[inferred]; @@ -30,12 +27,6 @@ export const LockfileSchema = type({ }); export type Lockfile = (typeof LockfileSchema)[inferred]; -export const ApiCliWhoamiResponseSchema = type({ - user: { - handle: "string|null", - }, -}); - export const ApiSearchResponseSchema = type({ results: type({ slug: "string?", diff --git a/dt-skill/test/cliCommandTestKit.ts b/dt-skill/test/cliCommandTestKit.ts index 5983dbb7..eb864d95 100644 --- a/dt-skill/test/cliCommandTestKit.ts +++ b/dt-skill/test/cliCommandTestKit.ts @@ -57,20 +57,6 @@ export function createRegistryModuleMocks() { }; } -export function createAuthTokenModuleMocks() { - const requireAuthToken = vi.fn(async () => "tkn"); - const getOptionalAuthToken = vi.fn(async () => undefined as string | undefined); - - return { - requireAuthToken, - getOptionalAuthToken, - moduleFactory: () => ({ - requireAuthToken: () => requireAuthToken(), - getOptionalAuthToken: () => getOptionalAuthToken(), - }), - }; -} - export function createUiModuleMocks(options?: { interactive?: boolean }) { const spinner = { stop: vi.fn(), From 12cf8302d4e3429a66b5a01fd09e69e79d2154ac Mon Sep 17 00:00:00 2001 From: huaiju Date: Sun, 14 Jun 2026 22:38:16 +0800 Subject: [PATCH 05/13] fix: avoid misclassifying valid UTF-8 as binary --- app/service/skills.js | 2 +- test/skills-import-package-name.test.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/service/skills.js b/app/service/skills.js index 8ca4cd83..7a984dc3 100644 --- a/app/service/skills.js +++ b/app/service/skills.js @@ -600,7 +600,7 @@ class SkillsService extends Service { const sample = buffer.subarray(0, Math.min(buffer.length, 4096)); if (sample.includes(0)) return true; try { - new TextDecoder('utf-8', { fatal: true }).decode(sample); + new TextDecoder('utf-8', { fatal: true }).decode(buffer); return false; } catch { return true; diff --git a/test/skills-import-package-name.test.js b/test/skills-import-package-name.test.js index c26f91f9..3553d730 100644 --- a/test/skills-import-package-name.test.js +++ b/test/skills-import-package-name.test.js @@ -11,6 +11,15 @@ test('isLikelyBinary treats invalid UTF-8 as binary so original bytes are preser assert.equal(service.isLikelyBinary(Buffer.from([0x61, 0x00, 0x62])), true); }); +test('isLikelyBinary does not reject valid UTF-8 split at the 4096-byte sample boundary', () => { + const service = Object.create(skillsModule.prototype); + const prefix = 'a'.repeat(4094); + const buffer = Buffer.from(`${prefix}答后续内容`, 'utf8'); + + assert.equal(buffer.subarray(0, 4096).toString('hex').endsWith('e7ad'), true); + assert.equal(service.isLikelyBinary(buffer), false); +}); + test('getUploadIdentityKey returns preferredName when provided', () => { const result = skillsModule.prototype.getUploadIdentityKey( [{ name: 'skill-a' }, { name: 'skill-b' }], From cb9cc15970b749dc2c1bee387fa9da5f407d573a Mon Sep 17 00:00:00 2001 From: huaiju Date: Sun, 14 Jun 2026 23:22:00 +0800 Subject: [PATCH 06/13] fix: preserve all skill files during publish and update --- dt-skill/src/cli/commands/publish.test.ts | 30 +++++++++ dt-skill/src/cli/commands/publish.ts | 6 +- dt-skill/src/cli/commands/skills.test.ts | 65 ++++++++++++++++++- dt-skill/src/cli/commands/skills.ts | 78 ++++++++++++++++++----- dt-skill/src/skills.ts | 33 ++++++++-- 5 files changed, 186 insertions(+), 26 deletions(-) diff --git a/dt-skill/src/cli/commands/publish.test.ts b/dt-skill/src/cli/commands/publish.test.ts index 547b6c4f..020b03ee 100644 --- a/dt-skill/src/cli/commands/publish.test.ts +++ b/dt-skill/src/cli/commands/publish.test.ts @@ -96,6 +96,36 @@ describe("cmdPublish", () => { } }); + it("发布时同时上传文本文件和二进制资源", async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, "skill-with-assets"); + await mkdir(join(folder, "assets"), { recursive: true }); + await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + await writeFile(join(folder, "assets", "logo.png"), new Uint8Array([0, 1, 2, 255])); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: "skill_1", + versionId: "ver_1", + }); + + await cmdPublish(makeOpts(workdir), "skill-with-assets", { + version: "1.0.0", + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls[0]; + const publishForm = (publishCall?.[1] as { form?: FormData }).form as FormData; + const files = publishForm.getAll("files") as Array; + expect(files.map((file) => file.name ?? "").sort()).toEqual([ + "SKILL.md", + "assets/logo.png", + ]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); + it("rejects oversized clawscan notes before uploading skill files", async () => { const workdir = await makeTmpWorkdir(); try { diff --git a/dt-skill/src/cli/commands/publish.ts b/dt-skill/src/cli/commands/publish.ts index 7bb7f613..6b8a84f2 100644 --- a/dt-skill/src/cli/commands/publish.ts +++ b/dt-skill/src/cli/commands/publish.ts @@ -8,7 +8,7 @@ import { ApiV1PublishResponseSchema, normalizeClawScanNote, } from "../../schema/index.js"; -import { listTextFiles } from "../../skills.js"; +import { listPublishFiles } from "../../skills.js"; import { getRegistry } from "../registry.js"; import { sanitizeSlug, titleCase } from "../slug.js"; import { findSkillFolders } from "../scanSkills.js"; @@ -85,7 +85,7 @@ export async function cmdPublish( const spinner = createSpinner(`Preparing ${slug}@${version}`); try { - const filesOnDisk = await ensureRootManifestFile(folder, await listTextFiles(folder)); + const filesOnDisk = await ensureRootManifestFile(folder, await listPublishFiles(folder)); if (filesOnDisk.length === 0) fail("No files found"); if ( !filesOnDisk.some((file) => { @@ -137,7 +137,7 @@ export async function cmdPublish( async function ensureRootManifestFile( folder: string, - files: Awaited>, + files: Awaited>, ) { if ( files.some((file) => { diff --git a/dt-skill/src/cli/commands/skills.test.ts b/dt-skill/src/cli/commands/skills.test.ts index d1fe25e6..534e8fb2 100644 --- a/dt-skill/src/cli/commands/skills.test.ts +++ b/dt-skill/src/cli/commands/skills.test.ts @@ -12,6 +12,8 @@ import * as skillStore from "../../skills.js"; const fsMocks = vi.hoisted(() => ({ mkdir: vi.fn(), + mkdtemp: vi.fn(), + rename: vi.fn(), rm: vi.fn(), stat: vi.fn(), })); @@ -21,6 +23,8 @@ vi.mock("node:fs/promises", async () => { return { ...actual, mkdir: fsMocks.mkdir, + mkdtemp: fsMocks.mkdtemp, + rename: fsMocks.rename, rm: fsMocks.rm, stat: fsMocks.stat, }; @@ -58,6 +62,8 @@ const writeLockfileMock = vi.spyOn(skillStore, "writeLockfile"); const writeSkillOriginMock = vi.spyOn(skillStore, "writeSkillOrigin"); const mkdirMock = fsMocks.mkdir; +const mkdtempMock = fsMocks.mkdtemp; +const renameMock = fsMocks.rename; const rmMock = fsMocks.rm; const statMock = fsMocks.stat; const { @@ -88,6 +94,8 @@ function makeOpts() { beforeEach(() => { mkdirMock.mockResolvedValue(undefined); + mkdtempMock.mockResolvedValue("/work/skills/.demo-update-test"); + renameMock.mockResolvedValue(undefined); rmMock.mockResolvedValue(undefined); statMock.mockRejectedValue(new Error("missing")); extractZipToDirMock.mockResolvedValue(undefined); @@ -286,6 +294,51 @@ describe("cmdUpdate", () => { expect(mockDownloadZip).not.toHaveBeenCalled(); }); + it("更新下载失败时保留现有技能", async () => { + mockApiRequest.mockResolvedValue({ latestVersion: { version: "2.0.0" }, moderation: null }); + mockDownloadZip.mockRejectedValue(new Error("download failed")); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(stat).mockResolvedValue({} as Awaited>); + + await expect(cmdUpdate(makeOpts(), "demo", { force: true }, false)).rejects.toThrow( + "download failed", + ); + + expect(rm).not.toHaveBeenCalledWith("/work/skills/demo", { + recursive: true, + force: true, + }); + expect(renameMock).not.toHaveBeenCalled(); + }); + + it("更新解压失败时保留现有技能", async () => { + mockApiRequest.mockResolvedValue({ latestVersion: { version: "2.0.0" }, moderation: null }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: "1.0.0", installedAt: 123 } }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(stat).mockResolvedValue({} as Awaited>); + vi.mocked(extractZipToDir).mockRejectedValue(new Error("extract failed")); + + await expect(cmdUpdate(makeOpts(), "demo", { force: true }, false)).rejects.toThrow( + "extract failed", + ); + + expect(renameMock).not.toHaveBeenCalled(); + expect(rm).not.toHaveBeenCalledWith("/work/skills/demo", { + recursive: true, + force: true, + }); + }); + it("skips pinned skills during update --all and reports them in the summary", async () => { mockApiRequest.mockResolvedValue({ latestVersion: { version: "2.0.0" }, @@ -388,7 +441,7 @@ describe("cmdUpdate", () => { "https://clawhub.ai", expect.objectContaining({ slug: "demo", version: "2.0.0" }), ); - expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/demo", { + expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/.demo-update-test", { version: 1, registry: "https://clawhub.ai", slug: "demo", @@ -396,6 +449,16 @@ describe("cmdUpdate", () => { installedAt: 123, fingerprint: "hash", }); + expect(renameMock).toHaveBeenNthCalledWith( + 1, + "/work/skills/demo", + "/work/skills/.demo-update-test-previous", + ); + expect(renameMock).toHaveBeenNthCalledWith( + 2, + "/work/skills/.demo-update-test", + "/work/skills/demo", + ); }); }); diff --git a/dt-skill/src/cli/commands/skills.ts b/dt-skill/src/cli/commands/skills.ts index 40c6f275..af3d71e1 100644 --- a/dt-skill/src/cli/commands/skills.ts +++ b/dt-skill/src/cli/commands/skills.ts @@ -1,5 +1,5 @@ -import { mkdir, rm, stat } from "node:fs/promises"; -import { join } from "node:path"; +import { mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises"; +import { basename, dirname, join } from "node:path"; import semver from "semver"; import { apiRequest, downloadZip, registryUrl } from "../../http.js"; import { @@ -534,24 +534,29 @@ export async function cmdUpdate( } else { spinner.start(`Updating ${entry} -> ${targetVersion}`); } - await rm(target, { recursive: true, force: true }); const zip = await downloadZip(registry, { slug: entry, version: targetVersion, }); - await extractZipToDir(zip, target); - const installedFiles = await listTextFiles(target); - const installedFingerprint = - installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; - - await writeSkillOrigin(target, { - version: 1, - registry: existingOrigin?.registry ?? registry, - slug: existingOrigin?.slug ?? entry, - installedVersion: targetVersion, - installedAt: existingOrigin?.installedAt ?? Date.now(), - fingerprint: installedFingerprint, - }); + const preparedDir = await prepareSkillUpdate(zip, target); + + try { + const installedFiles = await listTextFiles(preparedDir); + const installedFingerprint = + installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; + await writeSkillOrigin(preparedDir, { + version: 1, + registry: existingOrigin?.registry ?? registry, + slug: existingOrigin?.slug ?? entry, + installedVersion: targetVersion, + installedAt: existingOrigin?.installedAt ?? Date.now(), + fingerprint: installedFingerprint, + }); + await replaceSkillDirectory(preparedDir, target, exists); + } catch (error) { + await rm(preparedDir, { recursive: true, force: true }).catch(() => {}); + throw error; + } lock.skills[entry] = withPinnedMetadata(targetVersion, Date.now(), lock.skills[entry]); const agentSuffix3 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; @@ -571,6 +576,47 @@ export async function cmdUpdate( } } +async function prepareSkillUpdate(zip: Uint8Array, target: string) { + await mkdir(dirname(target), { recursive: true }); + const preparedDir = await mkdtemp(join(dirname(target), `.${basename(target)}-update-`)); + try { + await extractZipToDir(zip, preparedDir); + return preparedDir; + } catch (error) { + await rm(preparedDir, { recursive: true, force: true }).catch(() => {}); + throw error; + } +} + +async function replaceSkillDirectory(preparedDir: string, target: string, targetExists: boolean) { + const backupDir = `${preparedDir}-previous`; + let movedExisting = false; + + try { + if (targetExists) { + await rename(target, backupDir); + movedExisting = true; + } + await rename(preparedDir, target); + } catch (error) { + if (movedExisting) { + try { + await rename(backupDir, target); + } catch (rollbackError) { + throw new AggregateError( + [error, rollbackError], + `Failed to replace ${target} and restore the previous installation`, + ); + } + } + throw error; + } + + if (movedExisting) { + await rm(backupDir, { recursive: true, force: true }).catch(() => {}); + } +} + export async function cmdList(opts: GlobalOpts) { // Prompt for target agent when --agent is not provided and interactive let installWorkdir = opts.workdir; diff --git a/dt-skill/src/skills.ts b/dt-skill/src/skills.ts index ed17607c..bd225dd5 100644 --- a/dt-skill/src/skills.ts +++ b/dt-skill/src/skills.ts @@ -47,12 +47,7 @@ export async function extractZipToDir(zipBytes: Uint8Array, targetDir: string) { export async function listTextFiles(root: string) { const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; - const absRoot = resolve(root); - const ig = ignore(); - ig.add([".git/", "node_modules/", `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`]); - await addIgnoreFile(ig, join(absRoot, ".gitignore")); - await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); - await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE)); + const { absRoot, ig } = await createSkillIgnoreMatcher(root); await walk(absRoot, async (absPath) => { const relPath = normalizePath(relative(absRoot, absPath)); @@ -69,6 +64,22 @@ export async function listTextFiles(root: string) { return files; } +export async function listPublishFiles(root: string) { + const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; + const { absRoot, ig } = await createSkillIgnoreMatcher(root); + + await walk(absRoot, async (absPath) => { + const relPath = normalizePath(relative(absRoot, absPath)); + if (!relPath) return; + if (ig.ignores(relPath)) return; + if (hasDotPathSegment(relPath)) return; + const buffer = await readFile(absPath); + const contentType = mime.getType(relPath) ?? "application/octet-stream"; + files.push({ relPath, bytes: new Uint8Array(buffer), contentType }); + }); + return files; +} + type SkillFileHash = { path: string; sha256: string; size: number }; export { buildSkillFingerprint, sha256Hex }; @@ -211,6 +222,16 @@ async function addIgnoreFile(ig: ReturnType, path: string) { } } +async function createSkillIgnoreMatcher(root: string) { + const absRoot = resolve(root); + const ig = ignore(); + ig.add([".git/", "node_modules/", `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`]); + await addIgnoreFile(ig, join(absRoot, ".gitignore")); + await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); + await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE)); + return { absRoot, ig }; +} + export async function listManualSkills(skillsDir: string, lockedSlugs: Set) { const manual: string[] = []; let entries; From ca80f0aab41adefe1fe9185603aa8cb02790c065 Mon Sep 17 00:00:00 2001 From: huaiju Date: Sun, 14 Jun 2026 23:22:34 +0800 Subject: [PATCH 07/13] fix: cache skills storage initialization --- app/middleware/skillsStorageReady.js | 10 +++- test/skills-storage-ready-middleware.test.js | 49 ++++++++++++++++++-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/app/middleware/skillsStorageReady.js b/app/middleware/skillsStorageReady.js index bc8c0fd4..0c904cef 100644 --- a/app/middleware/skillsStorageReady.js +++ b/app/middleware/skillsStorageReady.js @@ -1,6 +1,14 @@ module.exports = () => { + let storageReadyPromise = null; + return async function skillsStorageReady(ctx, next) { - await ctx.service.skills.ensureStorageReady(); + if (!storageReadyPromise) { + storageReadyPromise = ctx.service.skills.ensureStorageReady().catch(error => { + storageReadyPromise = null; + throw error; + }); + } + await storageReadyPromise; await next(); }; }; diff --git a/test/skills-storage-ready-middleware.test.js b/test/skills-storage-ready-middleware.test.js index ca704969..7a2893ff 100644 --- a/test/skills-storage-ready-middleware.test.js +++ b/test/skills-storage-ready-middleware.test.js @@ -1,9 +1,10 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const skillsStorageReady = require('../app/middleware/skillsStorageReady')(); +const createSkillsStorageReady = require('../app/middleware/skillsStorageReady'); -test('skillsStorageReady middleware runs migration before the route handler', async () => { +test('skillsStorageReady 中间件在路由处理前完成数据库迁移', async () => { + const skillsStorageReady = createSkillsStorageReady(); const events = []; const ctx = { service: { @@ -22,7 +23,8 @@ test('skillsStorageReady middleware runs migration before the route handler', as assert.deepEqual(events, ['migrate', 'handler']); }); -test('skillsStorageReady middleware propagates handler errors after migration', async () => { +test('skillsStorageReady 中间件在迁移后继续抛出路由处理错误', async () => { + const skillsStorageReady = createSkillsStorageReady(); const ctx = { service: { skills: { @@ -39,3 +41,44 @@ test('skillsStorageReady middleware propagates handler errors after migration', /handler failed/ ); }); + +test('skillsStorageReady 中间件在多个请求之间只初始化一次存储', async () => { + let migrationCount = 0; + const middleware = createSkillsStorageReady(); + const createCtx = () => ({ + service: { + skills: { + ensureStorageReady: async () => { + migrationCount += 1; + }, + }, + }, + }); + + await middleware(createCtx(), async () => {}); + await middleware(createCtx(), async () => {}); + + assert.equal(migrationCount, 1); +}); + +test('skillsStorageReady 中间件在初始化失败后允许重试', async () => { + let migrationCount = 0; + const middleware = createSkillsStorageReady(); + const ctx = { + service: { + skills: { + ensureStorageReady: async () => { + migrationCount += 1; + if (migrationCount === 1) { + throw new Error('migration failed'); + } + }, + }, + }, + }; + + await assert.rejects(() => middleware(ctx, async () => {}), /migration failed/); + await middleware(ctx, async () => {}); + + assert.equal(migrationCount, 2); +}); From 7892215de42504c716317a9e42d7b2217f40f085 Mon Sep 17 00:00:00 2001 From: huaiju Date: Mon, 15 Jun 2026 00:15:30 +0800 Subject: [PATCH 08/13] fix: bundle skill fingerprint contract in dt-skill --- dt-skill/package.json | 4 +- dt-skill/scripts/build.mjs | 11 ++++- .../src/schema/skillFingerprintContract.ts | 8 +++- dt-skill/test-artifact/cli.artifact.test.ts | 40 ++++++++++++++----- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/dt-skill/package.json b/dt-skill/package.json index 4b04a5b6..b92031ea 100644 --- a/dt-skill/package.json +++ b/dt-skill/package.json @@ -1,6 +1,6 @@ { "name": "dt-skill", - "version": "0.18.2", + "version": "0.18.3", "description": "ClawHub CLI \\u2014 install, update, search, and publish skills plus OpenClaw packages.", "homepage": "https://clawhub.ai", "bugs": { @@ -31,7 +31,7 @@ "prepublishOnly": "npm run build", "test": "bun run test:src", "test:artifact": "bun run build && vitest run -c vitest.artifact.config.ts", - "test:src": "vitest run -c vitest.config.ts", + "test:src": "node ./scripts/build.mjs && vitest run -c vitest.config.ts", "verify": "bun run test:src && bun run verify:build && bun run test:artifact", "verify:build": "tsc -p tsconfig.json --noEmit" }, diff --git a/dt-skill/scripts/build.mjs b/dt-skill/scripts/build.mjs index 61b0e693..75753de5 100644 --- a/dt-skill/scripts/build.mjs +++ b/dt-skill/scripts/build.mjs @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { rm } from "node:fs/promises"; +import { cp, rename, rm } from "node:fs/promises"; import { createRequire } from "node:module"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -7,6 +7,8 @@ import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const distDir = resolve(packageRoot, "dist"); +const contractSourceDir = resolve(packageRoot, "..", "contracts", "skill-fingerprint"); +const contractDistDir = resolve(distDir, "contracts", "skill-fingerprint"); await rm(distDir, { recursive: true, force: true }); @@ -16,4 +18,9 @@ const result = spawnSync(process.execPath, [tscBin, "-p", "tsconfig.json"], { stdio: "inherit", }); -process.exit(result.status ?? 1); +if (result.status !== 0) { + process.exit(result.status ?? 1); +} + +await cp(contractSourceDir, contractDistDir, { recursive: true }); +await rename(resolve(contractDistDir, "index.js"), resolve(contractDistDir, "index.cjs")); diff --git a/dt-skill/src/schema/skillFingerprintContract.ts b/dt-skill/src/schema/skillFingerprintContract.ts index a02a0b99..dc4dfb7c 100644 --- a/dt-skill/src/schema/skillFingerprintContract.ts +++ b/dt-skill/src/schema/skillFingerprintContract.ts @@ -1,8 +1,14 @@ import { createRequire } from "node:module"; +import { dirname, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); +const currentDir = dirname(fileURLToPath(import.meta.url)); +const contractPath = currentDir.endsWith(`${sep}src${sep}schema`) + ? resolve(currentDir, "../../dist/contracts/skill-fingerprint/index.cjs") + : resolve(currentDir, "../contracts/skill-fingerprint/index.cjs"); -const contract = require("../../../contracts/skill-fingerprint/index.js"); +const contract = require(contractPath); export const TEXT_FILE_EXTENSIONS = contract.TEXT_FILE_EXTENSIONS as readonly string[]; export const TEXT_FILE_EXTENSION_SET = contract.TEXT_FILE_EXTENSION_SET as ReadonlySet; diff --git a/dt-skill/test-artifact/cli.artifact.test.ts b/dt-skill/test-artifact/cli.artifact.test.ts index b16df1ed..72136e88 100644 --- a/dt-skill/test-artifact/cli.artifact.test.ts +++ b/dt-skill/test-artifact/cli.artifact.test.ts @@ -1,7 +1,7 @@ /* @vitest-environment node */ import { spawnSync } from 'node:child_process'; -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -54,9 +54,30 @@ describe('built CLI artifact', () => { expect(result.stdout).toContain('dt-skill CLI'); }); - it('publishes a local code plugin in dry-run json mode from built output', async () => { + it('loads the fingerprint contract from the built package', async () => { + const isolatedPackage = await makeTmpDir('clawhub-artifact-package-'); + await cp(join(packageRoot, 'bin'), join(isolatedPackage, 'bin'), { recursive: true }); + await cp(join(packageRoot, 'dist'), join(isolatedPackage, 'dist'), { recursive: true }); + await cp(join(packageRoot, 'package.json'), join(isolatedPackage, 'package.json')); + + const result = spawnSync( + 'node', + [ + '--input-type=module', + '--eval', + `import('${join(isolatedPackage, 'dist/schema/skillFingerprintContract.js').replaceAll('\\', '\\\\')}')`, + ], + { encoding: 'utf8' } + ); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(''); + }); + + it('packs a local code plugin in json mode from built output', async () => { const root = await makeTmpDir('clawhub-artifact-'); const pluginDir = join(root, 'demo-plugin'); + const packDestination = join(root, 'packs'); await mkdir(join(pluginDir, 'src'), { recursive: true }); await writeFile( join(pluginDir, 'package.json'), @@ -107,25 +128,22 @@ describe('built CLI artifact', () => { [ binPath, 'package', - 'publish', + 'pack', pluginDir, - '--dry-run', '--json', - '--registry', - 'https://clawhub.ai', - '--site', - 'https://clawhub.ai', + '--pack-destination', + packDestination, ], { NPM_CONFIG_CACHE: join(root, '.npm-cache') } ); - expect(result.status).toBe(0); + expect(result.status, result.stderr || result.stdout).toBe(0); expect(result.stderr).toBe(''); const output = JSON.parse(result.stdout.trim()) as Record; expect(output.name).toBe('@openclaw/demo-plugin'); - expect(output.family).toBe('code-plugin'); expect(output.version).toBe('1.0.0'); - expect(output.commit).toBeTypeOf('string'); + expect(output.path).toBeTypeOf('string'); + expect(output.sha256).toBeTypeOf('string'); }); it('keeps the built dist free of compiled test files', async () => { From b466c634fdc6af60dd2e8fd53444a0200d4ebe88 Mon Sep 17 00:00:00 2001 From: huaiju Date: Tue, 16 Jun 2026 11:05:19 +0800 Subject: [PATCH 09/13] refactor(dt-skill): remove upstream package, sync, and clawdbot code Drop unimplemented OpenClaw package commands, bulk sync workflow, and clawdbot workspace discovery so the CLI focuses on core skill operations. Co-authored-by: Cursor --- dt-skill/src/clawpack.ts | 145 --- dt-skill/src/cli.ts | 187 +-- dt-skill/src/cli/clawdbotConfig.test.ts | 238 ---- dt-skill/src/cli/clawdbotConfig.ts | 204 ---- dt-skill/src/cli/commands/packages.ts | 1032 ----------------- dt-skill/src/cli/commands/publish.test.ts | 6 +- dt-skill/src/cli/commands/publish.ts | 4 +- dt-skill/src/cli/commands/sync.test.ts | 538 --------- dt-skill/src/cli/commands/sync.ts | 214 ---- dt-skill/src/cli/commands/syncHelpers.test.ts | 26 - dt-skill/src/cli/commands/syncHelpers.ts | 408 ------- dt-skill/src/cli/commands/syncTypes.ts | 27 - dt-skill/src/cli/scanSkills.test.ts | 13 +- dt-skill/src/cli/scanSkills.ts | 53 - dt-skill/src/schema/index.ts | 2 - dt-skill/src/schema/openclawContract.ts | 163 --- dt-skill/src/schema/packages.ts | 738 ------------ dt-skill/src/schema/routes.ts | 3 - dt-skill/test-artifact/cli.artifact.test.ts | 87 +- dt-skill/vitest.config.ts | 2 +- 20 files changed, 22 insertions(+), 4068 deletions(-) delete mode 100644 dt-skill/src/clawpack.ts delete mode 100644 dt-skill/src/cli/clawdbotConfig.test.ts delete mode 100644 dt-skill/src/cli/clawdbotConfig.ts delete mode 100644 dt-skill/src/cli/commands/packages.ts delete mode 100644 dt-skill/src/cli/commands/sync.test.ts delete mode 100644 dt-skill/src/cli/commands/sync.ts delete mode 100644 dt-skill/src/cli/commands/syncHelpers.test.ts delete mode 100644 dt-skill/src/cli/commands/syncHelpers.ts delete mode 100644 dt-skill/src/cli/commands/syncTypes.ts delete mode 100644 dt-skill/src/schema/openclawContract.ts delete mode 100644 dt-skill/src/schema/packages.ts diff --git a/dt-skill/src/clawpack.ts b/dt-skill/src/clawpack.ts deleted file mode 100644 index 7a78a33a..00000000 --- a/dt-skill/src/clawpack.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { gunzipSync } from "fflate"; - -type ClawPackEntry = { - path: string; - bytes: Uint8Array; -}; - -type ParsedClawPack = { - packageName: string; - packageVersion: string; - entries: ClawPackEntry[]; - packageJson: Record; - pluginManifest: Record; -}; - -const TAR_BLOCK_SIZE = 512; - -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - -function textFromBytes(bytes: Uint8Array) { - return new TextDecoder().decode(bytes); -} - -function readTarString(block: Uint8Array, offset: number, length: number) { - const slice = block.subarray(offset, offset + length); - const end = slice.indexOf(0); - return textFromBytes(end === -1 ? slice : slice.subarray(0, end)).trim(); -} - -function readTarSize(block: Uint8Array) { - const raw = readTarString(block, 124, 12).split("\0").join("").trim(); - if (!raw) return 0; - const size = Number.parseInt(raw, 8); - if (!Number.isFinite(size) || size < 0) throw new Error("Invalid tar entry size"); - return size; -} - -function normalizeTarPath(path: string) { - const normalized = path.replaceAll("\\", "/").replace(/^\.\/+/, ""); - if (!normalized || normalized.startsWith("/") || normalized.includes("\0")) return null; - const segments = normalized.split("/").filter(Boolean); - if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) { - return null; - } - return segments.join("/"); -} - -function isZeroBlock(block: Uint8Array) { - return block.every((byte) => byte === 0); -} - -function nextTarOffset(offset: number, size: number) { - return offset + Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; -} - -function parseTarEntries(bytes: Uint8Array): ClawPackEntry[] { - const entries: ClawPackEntry[] = []; - let offset = 0; - - while (offset + TAR_BLOCK_SIZE <= bytes.byteLength) { - const header = bytes.subarray(offset, offset + TAR_BLOCK_SIZE); - if (isZeroBlock(header)) break; - - const name = readTarString(header, 0, 100); - const prefix = readTarString(header, 345, 155); - const path = normalizeTarPath(prefix ? `${prefix}/${name}` : name); - if (!path) throw new Error("ClawPack contains an unsafe tar path"); - - const size = readTarSize(header); - const payloadOffset = offset + TAR_BLOCK_SIZE; - const payloadEnd = payloadOffset + size; - if (payloadEnd > bytes.byteLength) throw new Error("ClawPack tar entry is truncated"); - - const typeflag = String.fromCharCode(header[156] ?? 0).replace("\0", ""); - if (typeflag === "" || typeflag === "0") { - if (!path.startsWith("package/")) { - throw new Error("ClawPack entries must be rooted under package/"); - } - const relPath = path.slice("package/".length); - if (relPath) { - entries.push({ - path: relPath, - bytes: Uint8Array.from(bytes.subarray(payloadOffset, payloadEnd)), - }); - } - } else if (typeflag !== "5") { - throw new Error("ClawPack may only contain regular files and directories"); - } - - offset = nextTarOffset(payloadOffset, size); - } - - if (entries.length === 0) throw new Error("ClawPack contains no files"); - return entries; -} - -export function parseClawPack(bytes: Uint8Array): ParsedClawPack { - let tarBytes: Uint8Array; - try { - tarBytes = gunzipSync(bytes); - } catch { - throw new Error("ClawPack must be a gzip-compressed npm pack tarball"); - } - - const entries = parseTarEntries(tarBytes); - const packageJsonEntry = entries.find((entry) => entry.path === "package.json"); - if (!packageJsonEntry) throw new Error("ClawPack must contain package/package.json"); - const pluginManifestEntry = entries.find((entry) => entry.path === "openclaw.plugin.json"); - if (!pluginManifestEntry) { - throw new Error("ClawPack must contain package/openclaw.plugin.json"); - } - - let packageJson: unknown; - try { - packageJson = JSON.parse(textFromBytes(packageJsonEntry.bytes)); - } catch { - throw new Error("ClawPack package.json is invalid JSON"); - } - if (!isRecord(packageJson)) throw new Error("ClawPack package.json must be an object"); - - const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : ""; - const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : ""; - if (!packageName) throw new Error("ClawPack package.json must declare a name"); - if (!packageVersion) throw new Error("ClawPack package.json must declare a version"); - - let pluginManifest: unknown; - try { - pluginManifest = JSON.parse(textFromBytes(pluginManifestEntry.bytes)); - } catch { - throw new Error("ClawPack openclaw.plugin.json is invalid JSON"); - } - if (!isRecord(pluginManifest)) { - throw new Error("ClawPack openclaw.plugin.json must be an object"); - } - - return { - packageName, - packageVersion, - entries, - packageJson, - pluginManifest, - }; -} diff --git a/dt-skill/src/cli.ts b/dt-skill/src/cli.ts index 81eb73bb..ca2d73ff 100644 --- a/dt-skill/src/cli.ts +++ b/dt-skill/src/cli.ts @@ -3,7 +3,6 @@ import { stat } from "node:fs/promises"; import { join, resolve } from "node:path"; import { Command } from "commander"; import { getCliBuildLabel, getCliVersion } from "./cli/buildInfo.js"; -import { resolveClawdbotDefaultWorkspace } from "./cli/clawdbotConfig.js"; import { cmdDeleteSkill, cmdHideSkill, @@ -11,15 +10,6 @@ import { cmdUnhideSkill, } from "./cli/commands/delete.js"; import { cmdInspect } from "./cli/commands/inspect.js"; -import { - cmdDownloadPackage, - cmdExplorePackages, - cmdInspectPackage, - cmdPackageMigrationStatus, - cmdPackageReadiness, - cmdPackPackage, - cmdVerifyPackage, -} from "./cli/commands/packages.js"; import { cmdPublish } from "./cli/commands/publish.js"; import { cmdExplore, @@ -32,7 +22,6 @@ import { cmdUpdate, } from "./cli/commands/skills.js"; import { cmdStarSkill } from "./cli/commands/star.js"; -import { cmdSync } from "./cli/commands/sync.js"; import { cmdUnstarSkill } from "./cli/commands/unstar.js"; import { isAgentName, listAgentNames, resolveAgentWorkdir } from "./cli/agents.js"; import { configureCommanderHelp, styleEnvBlock, styleTitle } from "./cli/helpStyle.js"; @@ -47,7 +36,7 @@ const program = new Command() .name("dt-skill") .description( `${styleTitle(`dt-skill CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock( - "install, update, search, and publish skills plus OpenClaw packages.", + "install, update, search, and publish agent skills.", )}`, ) .version(getCliVersion(), "-V, --cli-version", "Show CLI version") @@ -63,7 +52,7 @@ const program = new Command() .addHelpText( "after", styleEnvBlock( - "\nEnv:\n CLAWHUB_SITE\n CLAWHUB_REGISTRY\n CLAWHUB_WORKDIR\n (CLAWDHUB_* supported)\n", + "\nEnv:\n DT_SKILL_SITE\n DT_SKILL_REGISTRY\n DT_SKILL_WORKDIR\n", ), ); @@ -109,17 +98,13 @@ async function resolveGlobalOpts(): Promise { dir = resolve(workdir, raw.dir ?? "skills"); } - const site = raw.site ?? process.env.CLAWHUB_SITE ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE; + const site = raw.site ?? process.env.DT_SKILL_SITE ?? DEFAULT_SITE; const registrySource = raw.registry ? "cli" - : process.env.CLAWHUB_REGISTRY || process.env.CLAWDHUB_REGISTRY + : process.env.DT_SKILL_REGISTRY ? "env" : "default"; - const registry = - raw.registry ?? - process.env.CLAWHUB_REGISTRY ?? - process.env.CLAWDHUB_REGISTRY ?? - DEFAULT_REGISTRY; + const registry = raw.registry ?? process.env.DT_SKILL_REGISTRY ?? DEFAULT_REGISTRY; return { workdir, dir, site, registry, registrySource, agent: agentName, globalScope: isGlobal, globalScopeExplicit: raw.global !== undefined }; } @@ -130,26 +115,19 @@ function isInputAllowed() { async function resolveWorkdir(explicit?: string) { if (explicit?.trim()) return resolve(explicit.trim()); - const envWorkdir = process.env.CLAWHUB_WORKDIR?.trim() ?? process.env.CLAWDHUB_WORKDIR?.trim(); + const envWorkdir = process.env.DT_SKILL_WORKDIR?.trim(); if (envWorkdir) return resolve(envWorkdir); const cwd = resolve(process.cwd()); - const hasMarker = await hasClawhubMarker(cwd); - if (hasMarker) return cwd; - - const clawdbotWorkspace = await resolveClawdbotDefaultWorkspace(); - return clawdbotWorkspace ? resolve(clawdbotWorkspace) : cwd; + if (await hasDtSkillMarker(cwd)) return cwd; + return cwd; } -async function hasClawhubMarker(workdir: string) { - const lockfile = join(workdir, ".clawhub", "lock.json"); +async function hasDtSkillMarker(workdir: string) { + const lockfile = join(workdir, ".dt-skill", "lock.json"); if (await pathExists(lockfile)) return true; - const markerDir = join(workdir, ".clawhub"); - if (await pathExists(markerDir)) return true; - const legacyLockfile = join(workdir, ".clawdhub", "lock.json"); - if (await pathExists(legacyLockfile)) return true; - const legacyMarkerDir = join(workdir, ".clawdhub"); - return pathExists(legacyMarkerDir); + const markerDir = join(workdir, ".dt-skill"); + return pathExists(markerDir); } async function pathExists(path: string) { @@ -345,116 +323,6 @@ registerCommand(skill, ["skill", "publish"]) await cmdPublish(opts, folder, options); }); -const packageCmd = registerCommandGroup(program, ["package"]).description( - "Browse OpenClaw packages", -); - -registerCommand(packageCmd, ["package", "explore"]) - .description("Browse published packages and plugins") - .argument("[query...]", "Optional search query") - .option("--family ", "skill|code-plugin|bundle-plugin") - .option("--official", "Only official packages") - .option("--executes-code", "Only packages that execute code") - .option("--target ", "Filter by host target, e.g. darwin-arm64") - .option("--os ", "Filter by host OS, e.g. darwin, linux, win32") - .option("--arch ", "Filter by host architecture, e.g. arm64 or x64") - .option("--libc ", "Filter by libc, e.g. glibc or musl") - .option("--requires-browser", "Only packages that require a browser") - .option("--requires-desktop", "Only packages that require local desktop access") - .option("--requires-native-deps", "Only packages with native dependency requirements") - .option("--requires-external-service", "Only packages that require an external service") - .option("--external-service ", "Filter by named external service") - .option("--binary ", "Filter by required local binary") - .option("--os-permission ", "Filter by required OS permission") - .option("--artifact-kind ", "legacy-zip|npm-pack") - .option("--npm-mirror", "Only packages available through the npm mirror") - .option( - "--limit ", - "Number of packages to show (max 100)", - (value) => Number.parseInt(value, 10), - 25, - ) - .option("--json", "Output JSON") - .action(async (queryParts, options) => { - const opts = await resolveGlobalOpts(); - const query = Array.isArray(queryParts) ? queryParts.join(" ").trim() : ""; - await cmdExplorePackages(opts, query, options); - }); - -registerCommand(packageCmd, ["package", "inspect"]) - .description("Fetch package metadata and files without installing") - .argument("", "Package name") - .option("--version ", "Version to inspect") - .option("--tag ", "Tag to inspect (default: latest)") - .option("--versions", "List version history (first page)") - .option("--limit ", "Max versions to list (1-100)", (value) => Number.parseInt(value, 10)) - .option("--files", "List files for the selected version") - .option("--file ", "Fetch raw file content (text only)") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdInspectPackage(opts, name, options); - }); - -registerCommand(packageCmd, ["package", "download"]) - .description("Download a package artifact and verify its published digests") - .argument("", "Package name") - .option("--version ", "Version to download") - .option("--tag ", "Tag to download (default: latest)") - .option("-o, --output ", "Output file or directory") - .option("--force", "Overwrite existing output file") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdDownloadPackage(opts, name, options); - }); - -registerCommand(packageCmd, ["package", "verify"]) - .description("Verify a local package artifact against ClawHub or expected digests") - .argument("", "Artifact file") - .option("--package ", "Package name to resolve expected artifact metadata") - .option("--version ", "Package version to resolve") - .option("--tag ", "Package tag to resolve") - .option("--sha256 ", "Expected ClawHub SHA-256") - .option("--npm-integrity ", "Expected npm sha512 integrity") - .option("--npm-shasum ", "Expected npm shasum") - .option("--json", "Output JSON") - .action(async (file, options) => { - const opts = await resolveGlobalOpts(); - await cmdVerifyPackage(opts, file, { - ...options, - packageName: options.package, - }); - }); - -registerCommand(packageCmd, ["package", "readiness"]) - .description("Check package readiness for future OpenClaw consumption") - .argument("", "Package name") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdPackageReadiness(opts, name, options); - }); - -registerCommand(packageCmd, ["package", "migration-status"]) - .description("Show package migration status for future OpenClaw consumption") - .argument("", "Package name") - .option("--json", "Output JSON") - .action(async (name, options) => { - const opts = await resolveGlobalOpts(); - await cmdPackageMigrationStatus(opts, name, options); - }); - -registerCommand(packageCmd, ["package", "pack"]) - .description("Create a ClawPack npm tarball from a plugin package folder") - .argument("", "Package folder path") - .option("--pack-destination ", "Directory for the generated .tgz (default: workdir)") - .option("--json", "Output JSON") - .action(async (source, options) => { - const opts = await resolveGlobalOpts(); - await cmdPackPackage(opts, source, options); - }); - registerCommand(program, ["star"]) .description("Add a skill to your highlights") .argument("", "Skill slug") @@ -473,37 +341,6 @@ registerCommand(program, ["unstar"]) await cmdUnstarSkill(opts, slug, options, isInputAllowed()); }); -registerCommand(program, ["sync"]) - .description("Scan local skills and publish new/updated ones") - .option("--root ", "Extra scan roots (one or more)") - .option("--all", "Upload all new/updated skills without prompting") - .option("--dry-run", "Show what would be uploaded") - .option("--bump ", "Version bump for updates (patch|minor|major)", "patch") - .option("--changelog ", "Changelog to use for updates (non-interactive)") - .option("--tags ", "Comma-separated tags", "latest") - .option("--concurrency ", "Concurrent registry checks (default: 4)", "4") - .action(async (options) => { - const opts = await resolveGlobalOpts(); - const bump = String(options.bump ?? "patch") as "patch" | "minor" | "major"; - if (!["patch", "minor", "major"].includes(bump)) fail("--bump must be patch|minor|major"); - const concurrencyRaw = Number(options.concurrency ?? 4); - const concurrency = Number.isFinite(concurrencyRaw) ? Math.round(concurrencyRaw) : 4; - if (concurrency < 1 || concurrency > 32) fail("--concurrency must be between 1 and 32"); - await cmdSync( - opts, - { - root: options.root, - all: options.all, - dryRun: options.dryRun, - bump, - changelog: options.changelog, - tags: options.tags, - concurrency, - }, - isInputAllowed(), - ); - }); - program.action(async () => { program.outputHelp(); process.exitCode = 0; diff --git a/dt-skill/src/cli/clawdbotConfig.test.ts b/dt-skill/src/cli/clawdbotConfig.test.ts deleted file mode 100644 index ef33f0eb..00000000 --- a/dt-skill/src/cli/clawdbotConfig.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* @vitest-environment node */ -import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; -import { resolveHome } from "../homedir.js"; -import { resolveClawdbotDefaultWorkspace, resolveClawdbotSkillRoots } from "./clawdbotConfig.js"; - -const originalEnv = { ...process.env }; - -afterEach(() => { - process.env = { ...originalEnv }; -}); - -describe("resolveClawdbotSkillRoots", () => { - it("reads JSON5 config and resolves per-agent + shared skill roots", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-")); - const home = join(base, "home"); - const stateDir = join(base, "state"); - const configPath = join(base, "clawdbot.json"); - const openclawStateDir = join(base, "openclaw-state"); - - process.env.HOME = home; - process.env.CLAWDBOT_STATE_DIR = stateDir; - process.env.CLAWDBOT_CONFIG_PATH = configPath; - process.env.OPENCLAW_STATE_DIR = openclawStateDir; - process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); - - const config = `{ - // JSON5 comments + trailing commas supported - agents: { - defaults: { workspace: '~/clawd-main', }, - list: [ - { id: 'work', name: 'Work Bot', workspace: '~/clawd-work', }, - { id: 'family', workspace: '~/clawd-family', }, - ], - }, - // legacy entries still supported - agent: { workspace: '~/clawd-legacy', }, - routing: { - agents: { - work: { name: 'Work Bot', workspace: '~/clawd-work', }, - family: { workspace: '~/clawd-family' }, - }, - }, - skills: { - load: { extraDirs: ['~/shared/skills', '/opt/skills',], }, - }, - }`; - await writeFile(configPath, config, "utf8"); - - const { roots, labels } = await resolveClawdbotSkillRoots(); - - const expectedRoots = [ - resolve(stateDir, "skills"), - resolve(openclawStateDir, "skills"), - resolve(home, "clawd-main", "skills"), - resolve(home, "clawd-work", "skills"), - resolve(home, "clawd-family", "skills"), - resolve(home, "shared", "skills"), - resolve("/opt/skills"), - ]; - - expect(roots).toEqual(expect.arrayContaining(expectedRoots)); - expect(labels[resolve(stateDir, "skills")]).toBe("Shared skills"); - expect(labels[resolve(openclawStateDir, "skills")]).toBe("OpenClaw: Shared skills"); - expect(labels[resolve(home, "clawd-main", "skills")]).toBe("Agent: main"); - expect(labels[resolve(home, "clawd-work", "skills")]).toBe("Agent: Work Bot"); - expect(labels[resolve(home, "clawd-family", "skills")]).toBe("Agent: family"); - expect(labels[resolve(home, "shared", "skills")]).toBe("Extra: skills"); - expect(labels[resolve("/opt/skills")]).toBe("Extra: skills"); - }); - - it("resolves default workspace from agents.defaults and agents.list", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-default-")); - const home = join(base, "home"); - const stateDir = join(base, "state"); - const configPath = join(base, "clawdbot.json"); - const workspaceMain = join(base, "workspace-main"); - const workspaceList = join(base, "workspace-list"); - const openclawStateDir = join(base, "openclaw-state"); - - process.env.HOME = home; - process.env.CLAWDBOT_STATE_DIR = stateDir; - process.env.CLAWDBOT_CONFIG_PATH = configPath; - process.env.OPENCLAW_STATE_DIR = openclawStateDir; - process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); - - const config = `{ - agents: { - defaults: { workspace: "${workspaceMain}", }, - list: [ - { id: 'main', workspace: "${workspaceList}", default: true }, - ], - }, - }`; - await writeFile(configPath, config, "utf8"); - - const workspace = await resolveClawdbotDefaultWorkspace(); - expect(workspace).toBe(resolve(workspaceMain)); - }); - - it("falls back to default agent in agents.list when defaults missing", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-list-")); - const home = join(base, "home"); - const configPath = join(base, "clawdbot.json"); - const workspaceMain = join(base, "workspace-main"); - const workspaceWork = join(base, "workspace-work"); - const openclawStateDir = join(base, "openclaw-state"); - - process.env.HOME = home; - process.env.CLAWDBOT_CONFIG_PATH = configPath; - process.env.OPENCLAW_STATE_DIR = openclawStateDir; - process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); - - const config = `{ - agents: { - list: [ - { id: 'main', workspace: "${workspaceMain}", default: true }, - { id: 'work', workspace: "${workspaceWork}" }, - ], - }, - }`; - await writeFile(configPath, config, "utf8"); - - const workspace = await resolveClawdbotDefaultWorkspace(); - expect(workspace).toBe(resolve(workspaceMain)); - }); - - it("respects CLAWDBOT_STATE_DIR and CLAWDBOT_CONFIG_PATH overrides", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-override-")); - const home = join(base, "home"); - const stateDir = join(base, "custom-state"); - const configPath = join(base, "config", "clawdbot.json"); - const openclawStateDir = join(base, "openclaw-state"); - - process.env.HOME = home; - process.env.CLAWDBOT_STATE_DIR = stateDir; - process.env.CLAWDBOT_CONFIG_PATH = configPath; - process.env.OPENCLAW_STATE_DIR = openclawStateDir; - process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); - - const config = `{ - agent: { workspace: "${join(base, "workspace-main")}" }, - }`; - await mkdir(join(base, "config"), { recursive: true }); - await writeFile(configPath, config, "utf8"); - - const { roots, labels } = await resolveClawdbotSkillRoots(); - - expect(roots).toEqual( - expect.arrayContaining([ - resolve(stateDir, "skills"), - resolve(openclawStateDir, "skills"), - resolve(join(base, "workspace-main"), "skills"), - ]), - ); - expect(labels[resolve(stateDir, "skills")]).toBe("Shared skills"); - expect(labels[resolve(openclawStateDir, "skills")]).toBe("OpenClaw: Shared skills"); - expect(labels[resolve(join(base, "workspace-main"), "skills")]).toBe("Agent: main"); - }); - - it("returns shared skills root when config is missing", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-clawdbot-missing-")); - const stateDir = join(base, "state"); - const configPath = join(base, "missing", "clawdbot.json"); - const openclawStateDir = join(base, "openclaw-state"); - - process.env.CLAWDBOT_STATE_DIR = stateDir; - process.env.CLAWDBOT_CONFIG_PATH = configPath; - process.env.OPENCLAW_STATE_DIR = openclawStateDir; - process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); - - const { roots, labels } = await resolveClawdbotSkillRoots(); - - expect(roots).toEqual([resolve(stateDir, "skills"), resolve(openclawStateDir, "skills")]); - expect(labels[resolve(stateDir, "skills")]).toBe("Shared skills"); - expect(labels[resolve(openclawStateDir, "skills")]).toBe("OpenClaw: Shared skills"); - }); - - it("uses $HOME over os.homedir() for tilde expansion", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-home-override-")); - const customHome = join(base, "custom-home"); - const stateDir = join(base, "state"); - const configPath = join(base, "clawdbot.json"); - const openclawStateDir = join(base, "openclaw-state"); - - process.env.HOME = customHome; - process.env.CLAWDBOT_STATE_DIR = stateDir; - process.env.CLAWDBOT_CONFIG_PATH = configPath; - process.env.OPENCLAW_STATE_DIR = openclawStateDir; - process.env.OPENCLAW_CONFIG_PATH = join(openclawStateDir, "openclaw.json"); - - const config = `{ - agents: { - defaults: { workspace: "~/my-workspace" }, - }, - }`; - await writeFile(configPath, config, "utf8"); - - const workspace = await resolveClawdbotDefaultWorkspace(); - expect(workspace).toBe(resolve(customHome, "my-workspace")); - expect(resolveHome()).toBe(customHome); - }); - - it("normalizes trailing separators in $HOME", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-home-trailing-")); - const customHome = join(base, "custom-home"); - - process.env.HOME = `${customHome}/`; - - expect(resolveHome()).toBe(customHome); - }); - - it("supports OpenClaw configuration files", async () => { - const base = await mkdtemp(join(tmpdir(), "clawhub-openclaw-")); - const stateDir = join(base, "openclaw-state"); - const workspace = join(base, "openclaw-main"); - const configPath = join(stateDir, "openclaw.json"); - - process.env.OPENCLAW_STATE_DIR = stateDir; - - await mkdir(stateDir, { recursive: true }); - const config = `{ - agents: { - defaults: { workspace: "${workspace}", }, - }, - }`; - await writeFile(configPath, config, "utf8"); - - const { roots, labels } = await resolveClawdbotSkillRoots(); - expect(roots).toEqual( - expect.arrayContaining([resolve(stateDir, "skills"), resolve(workspace, "skills")]), - ); - expect(labels[resolve(stateDir, "skills")]).toBe("OpenClaw: Shared skills"); - expect(labels[resolve(workspace, "skills")]).toBe("OpenClaw: Agent: main"); - }); -}); diff --git a/dt-skill/src/cli/clawdbotConfig.ts b/dt-skill/src/cli/clawdbotConfig.ts deleted file mode 100644 index 4decb342..00000000 --- a/dt-skill/src/cli/clawdbotConfig.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { readFile } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; -import JSON5 from "json5"; -import { resolveHome } from "../homedir.js"; - -type ClawdbotConfig = { - agent?: { workspace?: string }; - agents?: { - defaults?: { workspace?: string }; - list?: Array<{ - id?: string; - name?: string; - workspace?: string; - default?: boolean; - }>; - }; - routing?: { - agents?: Record< - string, - { - name?: string; - workspace?: string; - } - >; - }; - skills?: { - load?: { - extraDirs?: string[]; - }; - }; -}; - -type ClawdbotSkillRoots = { - roots: string[]; - labels: Record; -}; - -export async function resolveClawdbotSkillRoots(): Promise { - const roots: string[] = []; - const labels: Record = {}; - - const clawdbotStateDir = resolveClawdbotStateDir(); - const sharedSkills = resolveUserPath(join(clawdbotStateDir, "skills")); - pushRoot(roots, labels, sharedSkills, "Shared skills"); - - const openclawStateDir = resolveOpenclawStateDir(); - const openclawShared = resolveUserPath(join(openclawStateDir, "skills")); - pushRoot(roots, labels, openclawShared, "OpenClaw: Shared skills"); - - const [clawdbotConfig, openclawConfig] = await Promise.all([ - readClawdbotConfig(), - readOpenclawConfig(), - ]); - if (!clawdbotConfig && !openclawConfig) return { roots, labels }; - - if (clawdbotConfig) { - addConfigRoots(clawdbotConfig, roots, labels); - } - if (openclawConfig) { - addConfigRoots(openclawConfig, roots, labels, "OpenClaw"); - } - - return { roots, labels }; -} - -export async function resolveClawdbotDefaultWorkspace(): Promise { - const config = await readClawdbotConfig(); - const openclawConfig = await readOpenclawConfig(); - if (!config && !openclawConfig) return null; - - const defaultsWorkspace = resolveUserPath( - config?.agents?.defaults?.workspace ?? config?.agent?.workspace ?? "", - ); - if (defaultsWorkspace) return defaultsWorkspace; - - const listedAgents = config?.agents?.list ?? []; - const defaultAgent = - listedAgents.find((entry) => entry.default) ?? - listedAgents.find((entry) => entry.id === "main"); - const listWorkspace = resolveUserPath(defaultAgent?.workspace ?? ""); - if (listWorkspace) return listWorkspace; - - if (!openclawConfig) return null; - const openclawDefaults = resolveUserPath( - openclawConfig.agents?.defaults?.workspace ?? openclawConfig.agent?.workspace ?? "", - ); - if (openclawDefaults) return openclawDefaults; - const openclawAgents = openclawConfig.agents?.list ?? []; - const openclawDefaultAgent = - openclawAgents.find((entry) => entry.default) ?? - openclawAgents.find((entry) => entry.id === "main"); - const openclawWorkspace = resolveUserPath(openclawDefaultAgent?.workspace ?? ""); - return openclawWorkspace || null; -} - -function resolveClawdbotStateDir() { - const override = process.env.CLAWDBOT_STATE_DIR?.trim(); - if (override) return resolveUserPath(override); - return join(resolveHome(), ".clawdbot"); -} - -function resolveClawdbotConfigPath() { - const override = process.env.CLAWDBOT_CONFIG_PATH?.trim(); - if (override) return resolveUserPath(override); - return join(resolveClawdbotStateDir(), "clawdbot.json"); -} - -function resolveOpenclawStateDir() { - const override = process.env.OPENCLAW_STATE_DIR?.trim(); - if (override) return resolveUserPath(override); - return join(resolveHome(), ".openclaw"); -} - -function resolveOpenclawConfigPath() { - const override = process.env.OPENCLAW_CONFIG_PATH?.trim(); - if (override) return resolveUserPath(override); - return join(resolveOpenclawStateDir(), "openclaw.json"); -} - -function resolveUserPath(input: string) { - const trimmed = input.trim(); - if (!trimmed) return ""; - if (trimmed.startsWith("~")) { - return resolve(trimmed.replace(/^~(?=$|[\\/])/, resolveHome())); - } - return resolve(trimmed); -} - -async function readClawdbotConfig(): Promise { - return readConfigFile(resolveClawdbotConfigPath()); -} - -async function readOpenclawConfig(): Promise { - return readConfigFile(resolveOpenclawConfigPath()); -} - -async function readConfigFile(path: string): Promise { - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON5.parse(raw); - if (!parsed || typeof parsed !== "object") return null; - return parsed as ClawdbotConfig; - } catch { - return null; - } -} - -function addConfigRoots( - config: ClawdbotConfig, - roots: string[], - labels: Record, - labelPrefix?: string, -) { - const prefix = labelPrefix ? `${labelPrefix}: ` : ""; - - const mainWorkspace = resolveUserPath( - config.agents?.defaults?.workspace ?? config.agent?.workspace ?? "", - ); - if (mainWorkspace) { - pushRoot(roots, labels, join(mainWorkspace, "skills"), `${prefix}Agent: main`); - } - - const listedAgents = config.agents?.list ?? []; - for (const entry of listedAgents) { - const workspace = resolveUserPath(entry?.workspace ?? ""); - if (!workspace) continue; - const name = entry?.name?.trim() || entry?.id?.trim() || "agent"; - pushRoot(roots, labels, join(workspace, "skills"), `${prefix}Agent: ${name}`); - } - - const agents = config.routing?.agents ?? {}; - for (const [agentId, entry] of Object.entries(agents)) { - const workspace = resolveUserPath(entry?.workspace ?? ""); - if (!workspace) continue; - const name = entry?.name?.trim() || agentId; - pushRoot(roots, labels, join(workspace, "skills"), `${prefix}Agent: ${name}`); - } - - const extraDirs = config.skills?.load?.extraDirs ?? []; - for (const dir of extraDirs) { - const resolved = resolveUserPath(dir); - if (!resolved) continue; - const label = `${prefix}Extra: ${basename(resolved) || resolved}`; - pushRoot(roots, labels, resolved, label); - } -} - -function pushRoot(roots: string[], labels: Record, root: string, label?: string) { - const resolved = resolveUserPath(root); - if (!resolved) return; - if (!roots.includes(resolved)) roots.push(resolved); - if (!label) return; - const existing = labels[resolved]; - if (!existing) { - labels[resolved] = label; - return; - } - const parts = existing - .split(", ") - .map((part) => part.trim()) - .filter(Boolean); - if (parts.includes(label)) return; - labels[resolved] = `${existing}, ${label}`; -} diff --git a/dt-skill/src/cli/commands/packages.ts b/dt-skill/src/cli/commands/packages.ts deleted file mode 100644 index 995362b5..00000000 --- a/dt-skill/src/cli/commands/packages.ts +++ /dev/null @@ -1,1032 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; -import { basename, dirname, join, relative, resolve, sep } from "node:path"; -import semver from "semver"; -import { parseClawPack } from "../../clawpack.js"; -import { apiRequest, fetchBinary, fetchText, registryUrl } from "../../http.js"; -import { - ApiRoutes, - ApiV1PackageArtifactResponseSchema, - ApiV1PackageListResponseSchema, - ApiV1PackageReadinessResponseSchema, - ApiV1PackageResponseSchema, - ApiV1PackageSearchResponseSchema, - ApiV1PackageVersionListResponseSchema, - ApiV1PackageVersionResponseSchema, - type PackageArtifactSummary, - type PackageCapabilitySummary, - type PackageCompatibility, - type PackageFamily, - type PackageVerificationSummary, - validateOpenClawExternalCodePluginPackageContents, - validateOpenClawExternalCodePluginPackageJson, -} from "../../schema/index.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError } from "../ui.js"; -import { resolveSourceInput } from "./github.js"; - -const MAX_CLAWPACK_BYTES = 120 * 1024 * 1024; - -type PackageInspectOptions = { - version?: string; - tag?: string; - versions?: boolean; - limit?: number; - files?: boolean; - file?: string; - json?: boolean; -}; - -type PackageExploreOptions = { - family?: PackageFamily; - official?: boolean; - executesCode?: boolean; - target?: string; - os?: string; - arch?: string; - libc?: string; - requiresBrowser?: boolean; - requiresDesktop?: boolean; - requiresNativeDeps?: boolean; - requiresExternalService?: boolean; - externalService?: string; - binary?: string; - osPermission?: string; - artifactKind?: "legacy-zip" | "npm-pack"; - npmMirror?: boolean; - limit?: number; - json?: boolean; -}; - -type PackagePackOptions = { - packDestination?: string; - json?: boolean; -}; - -type PackageDownloadOptions = { - version?: string; - tag?: string; - output?: string; - force?: boolean; - json?: boolean; -}; - -type PackageVerifyOptions = { - packageName?: string; - version?: string; - tag?: string; - sha256?: string; - npmIntegrity?: string; - npmShasum?: string; - json?: boolean; -}; - -type PackageReadinessOptions = { - json?: boolean; -}; - -type PackageMigrationStatusOptions = PackageReadinessOptions; - -type PackageFile = { - relPath: string; - bytes: Uint8Array; - contentType?: string; -}; - -type PackedClawPack = { - path: string; - file: PackageFile; - parsed: ReturnType; - identity: ArtifactIdentity; -}; - -function appendPackageExploreFilters(url: URL, options: PackageExploreOptions) { - if (options.target) url.searchParams.set("target", options.target); - if (options.os) url.searchParams.set("os", options.os); - if (options.arch) url.searchParams.set("arch", options.arch); - if (options.libc) url.searchParams.set("libc", options.libc); - if (options.requiresBrowser) url.searchParams.set("requiresBrowser", "true"); - if (options.requiresDesktop) url.searchParams.set("requiresDesktop", "true"); - if (options.requiresNativeDeps) url.searchParams.set("requiresNativeDeps", "true"); - if (options.requiresExternalService) url.searchParams.set("requiresExternalService", "true"); - if (options.externalService) url.searchParams.set("externalService", options.externalService); - if (options.binary) url.searchParams.set("binary", options.binary); - if (options.osPermission) url.searchParams.set("osPermission", options.osPermission); - if (options.artifactKind) url.searchParams.set("artifactKind", options.artifactKind); - if (options.npmMirror) url.searchParams.set("npmMirror", "true"); -} - -type PrintableFile = { - path: string; - size: number | null; - sha256: string | null; - contentType: string | null; -}; - -type PackageResponse = Awaited>; -type PackageVersionResponse = Awaited>; -type PackageArtifactResponse = Awaited>; -type ArtifactIdentity = { - sha256: string; - npmIntegrity: string; - npmShasum: string; - byteLength: number; -}; - -export async function cmdExplorePackages( - opts: GlobalOpts, - query: string, - options: PackageExploreOptions = {}, -) { - const trimmedQuery = query.trim(); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(trimmedQuery ? "Searching packages" : "Listing packages"); - try { - const limit = clampLimit(options.limit ?? 25, 100); - if (trimmedQuery) { - const url = registryUrl(`${ApiRoutes.packages}/search`, registry); - url.searchParams.set("q", trimmedQuery); - url.searchParams.set("limit", String(limit)); - if (options.family) url.searchParams.set("family", options.family); - if (options.official) url.searchParams.set("isOfficial", "true"); - if (typeof options.executesCode === "boolean") { - url.searchParams.set("executesCode", String(options.executesCode)); - } - appendPackageExploreFilters(url, options); - const result = await apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1PackageSearchResponseSchema, - ); - spinner.stop(); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - if (result.results.length === 0) { - console.log("No packages found."); - return; - } - for (const entry of result.results) { - console.log(formatPackageLine(entry.package)); - } - return; - } - - const route = - options.family === "code-plugin" - ? ApiRoutes.codePlugins - : options.family === "bundle-plugin" - ? ApiRoutes.bundlePlugins - : ApiRoutes.packages; - const url = registryUrl(route, registry); - url.searchParams.set("limit", String(limit)); - if (options.family === "skill") url.searchParams.set("family", "skill"); - if (options.official) url.searchParams.set("isOfficial", "true"); - if (typeof options.executesCode === "boolean") { - url.searchParams.set("executesCode", String(options.executesCode)); - } - appendPackageExploreFilters(url, options); - const result = await apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1PackageListResponseSchema, - ); - spinner.stop(); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - if (result.items.length === 0) { - console.log("No packages found."); - return; - } - for (const item of result.items) { - console.log(formatPackageLine(item)); - } - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdInspectPackage( - opts: GlobalOpts, - packageName: string, - options: PackageInspectOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - if (options.version && options.tag) fail("Use either --version or --tag"); - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Fetching package"); - try { - const detail = await apiRequestPackageDetail(registry, trimmed); - if (!detail.package) { - spinner.fail("Package not found"); - return; - } - - const tags = normalizeTags(detail.package.tags); - const latestVersion = detail.package.latestVersion ?? tags.latest ?? null; - const taggedVersion = options.tag ? (tags[options.tag] ?? null) : null; - if (options.tag && !taggedVersion) { - spinner.fail(`Unknown tag "${options.tag}"`); - return; - } - const requestedVersion = options.version ?? taggedVersion ?? null; - - let versionResult: PackageVersionResponse | null = null; - if (options.files || options.file || options.version || options.tag) { - const targetVersion = requestedVersion ?? latestVersion; - if (!targetVersion) fail("Could not resolve latest version"); - spinner.text = `Fetching ${trimmed}@${targetVersion}`; - versionResult = await apiRequestPackageVersion(registry, trimmed, targetVersion); - } - - let versionsList: Awaited> | null = null; - if (options.versions) { - const limit = clampLimit(options.limit ?? 25, 100); - spinner.text = `Fetching versions (${limit})`; - versionsList = await apiRequestPackageVersions(registry, trimmed, limit); - } - - let fileContent: string | null = null; - if (options.file) { - const url = registryUrl( - `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/file`, - registry, - ); - url.searchParams.set("path", options.file); - if (options.version) { - url.searchParams.set("version", options.version); - } else if (options.tag) { - url.searchParams.set("tag", options.tag); - } else if (latestVersion) { - url.searchParams.set("version", latestVersion); - } - spinner.text = `Fetching ${options.file}`; - fileContent = await fetchText(registry, { url: url.toString() }); - } - - spinner.stop(); - - const output = { - package: detail.package, - owner: detail.owner, - version: versionResult?.version ?? null, - versions: versionsList?.items ?? null, - file: options.file ? { path: options.file, content: fileContent } : null, - }; - - if (options.json) { - console.log(JSON.stringify(output, null, 2)); - return; - } - - const shouldPrintMeta = !options.file || options.files || options.versions || options.version; - if (shouldPrintMeta) { - printPackageSummary(detail); - } - - if (shouldPrintMeta && versionResult?.version) { - printVersionSummary(versionResult.version); - printCompatibility( - versionResult.version.compatibility ?? detail.package.compatibility ?? null, - ); - printCapabilities(versionResult.version.capabilities ?? detail.package.capabilities ?? null); - printVerification(versionResult.version.verification ?? detail.package.verification ?? null); - printArtifact(versionResult.version.artifact ?? detail.package.artifact ?? null); - } else if (shouldPrintMeta) { - printCompatibility(detail.package.compatibility ?? null); - printCapabilities(detail.package.capabilities ?? null); - printVerification(detail.package.verification ?? null); - printArtifact(detail.package.artifact ?? null); - } - - if (versionsList?.items) { - if (versionsList.items.length === 0) { - console.log("No versions found."); - } else { - console.log("Versions:"); - for (const item of versionsList.items) { - console.log(`- ${item.version} ${formatTimestamp(item.createdAt)}`); - } - } - } - - if (versionResult?.version && options.files) { - const files = normalizeFiles(versionResult.version.files); - if (files.length === 0) { - console.log("No files found."); - } else { - console.log("Files:"); - for (const file of files) { - console.log(formatFileLine(file)); - } - } - } - - if (options.file && fileContent !== null) { - if (shouldPrintMeta) console.log(`\n${options.file}:\n`); - process.stdout.write(fileContent); - if (!fileContent.endsWith("\n")) process.stdout.write("\n"); - } - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdPackPackage( - opts: GlobalOpts, - sourceArg: string, - options: PackagePackOptions = {}, -) { - if (!sourceArg?.trim()) fail("Path required"); - const resolvedSource = await resolveSourceInput(sourceArg, { - workdir: opts.workdir, - localWorkdirs: [process.cwd(), opts.workdir], - }); - if (resolvedSource.kind !== "local") fail("Path must be a package folder"); - const sourcePath = resolvedSource.path; - const sourceStat = await stat(sourcePath).catch(() => null); - if (!sourceStat?.isDirectory()) fail("Path must be a package folder"); - - const packageJson = await readJsonFile(join(sourcePath, "package.json")); - if (!packageJson) fail("package.json required"); - const pluginManifest = await readJsonFile(join(sourcePath, "openclaw.plugin.json")); - if (!pluginManifest) fail("openclaw.plugin.json required"); - - const packageName = packageJsonString(packageJson, "name"); - const packageVersion = packageJsonString(packageJson, "version"); - if (!packageName) fail("package.json name required"); - if (!packageVersion) fail("package.json version required"); - if (!semver.valid(packageVersion)) fail("package.json version must be valid semver"); - - const validation = validateOpenClawExternalCodePluginPackageJson(packageJson); - if (validation.issues.length > 0) { - fail(validation.issues.map((issue) => issue.message).join(" ")); - } - - const packDestination = resolve(opts.workdir, options.packDestination ?? "."); - await mkdir(packDestination, { recursive: true }); - - const spinner = options.json ? null : createSpinner(`Packing ${packageName}@${packageVersion}`); - try { - const packed = await createClawPackFromFolder({ - sourcePath, - packDestination, - cwd: opts.workdir, - }); - const contentValidation = validateOpenClawExternalCodePluginPackageContents( - packed.parsed.packageJson, - packed.parsed.entries.map((entry) => entry.path), - ); - if (contentValidation.issues.length > 0) { - fail(contentValidation.issues.map((issue) => issue.message).join(" ")); - } - const output = { - path: packed.path, - name: packed.parsed.packageName, - version: packed.parsed.packageVersion, - size: packed.file.bytes.byteLength, - files: packed.parsed.entries.length, - sha256: packed.identity.sha256, - npmIntegrity: packed.identity.npmIntegrity, - npmShasum: packed.identity.npmShasum, - }; - - spinner?.succeed( - `Packed ${packed.parsed.packageName}@${packed.parsed.packageVersion} -> ${packed.path}`, - ); - if (options.json) { - process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); - } else { - console.log(`Path: ${packed.path}`); - console.log(`Size: ${packed.file.bytes.byteLength} bytes`); - console.log(`SHA-256: ${packed.identity.sha256}`); - console.log(`npm integrity: ${packed.identity.npmIntegrity}`); - } - } catch (error) { - spinner?.fail(formatError(error)); - throw error; - } -} - -async function createClawPackFromFolder(options: { - sourcePath: string; - packDestination: string; - cwd: string; -}): Promise { - const result = spawnSync( - "npm", - [ - "pack", - options.sourcePath, - "--json", - "--ignore-scripts", - "--pack-destination", - options.packDestination, - ], - { - cwd: options.cwd, - encoding: "utf8", - }, - ); - if (result.error) throw result.error; - if (result.status !== 0) { - fail((result.stderr || result.stdout || "npm pack failed").trim()); - } - - let npmOutput: Array<{ filename?: string }> = []; - try { - npmOutput = JSON.parse(result.stdout) as Array<{ filename?: string }>; - } catch { - fail("npm pack did not return JSON output"); - } - const filename = npmOutput[0]?.filename; - if (!filename) fail("npm pack did not return a tarball filename"); - - const packPath = resolve(options.packDestination, filename); - const bytes = new Uint8Array(await readFile(packPath)); - assertClawPackSize(bytes.byteLength, basename(packPath)); - const parsed = parseClawPack(bytes); - return { - path: packPath, - file: { - relPath: basename(packPath), - bytes, - contentType: "application/octet-stream", - }, - parsed, - identity: computeArtifactIdentity(bytes), - }; -} - -export async function cmdDownloadPackage( - opts: GlobalOpts, - packageName: string, - options: PackageDownloadOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - if (options.version && options.tag) fail("Use either --version or --tag"); - const registry = await getRegistry(opts, { cache: true }); - const spinner = options.json ? null : createSpinner("Resolving package artifact"); - try { - const targetVersion = await resolvePackageVersion(registry, trimmed, { - version: options.version, - tag: options.tag, - }); - spinnerText(spinner, `Resolving ${trimmed}@${targetVersion}`); - const artifactResult = await apiRequestPackageArtifact(registry, trimmed, targetVersion); - spinnerText(spinner, `Downloading ${trimmed}@${targetVersion}`); - const bytes = await fetchBinary(registry, { - url: artifactResult.artifact.downloadUrl, - }); - const identity = computeArtifactIdentity(bytes); - validateDownloadedArtifact(trimmed, artifactResult, bytes, identity); - - const filename = defaultArtifactFilename(trimmed, targetVersion, artifactResult.artifact); - const outputPath = await resolveArtifactOutputPath(opts, options.output, filename); - await assertOutputWritable(outputPath, Boolean(options.force)); - await writeFile(outputPath, bytes); - spinner?.stop(); - - const output = { - package: artifactResult.package.name, - version: targetVersion, - artifact: artifactResult.artifact, - path: outputPath, - bytes: bytes.byteLength, - sha256: identity.sha256, - npmIntegrity: artifactResult.artifact.kind === "npm-pack" ? identity.npmIntegrity : undefined, - npmShasum: artifactResult.artifact.kind === "npm-pack" ? identity.npmShasum : undefined, - }; - if (options.json) { - process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); - return; - } - console.log(`Downloaded ${artifactResult.package.name}@${targetVersion} -> ${outputPath}`); - console.log(`Artifact: ${artifactResult.artifact.kind}`); - console.log(`SHA-256: ${identity.sha256}`); - if (artifactResult.artifact.kind === "npm-pack") { - console.log(`npm integrity: ${identity.npmIntegrity}`); - console.log(`npm shasum: ${identity.npmShasum}`); - } - } catch (error) { - spinner?.fail(formatError(error)); - throw error; - } -} - -export async function cmdVerifyPackage( - opts: GlobalOpts, - filePath: string, - options: PackageVerifyOptions = {}, -) { - const targetFile = resolve(opts.workdir, filePath); - if (options.version && options.tag) fail("Use either --version or --tag"); - if ((options.version || options.tag) && !options.packageName?.trim()) { - fail("--package is required with --version or --tag"); - } - - const spinner = options.json ? null : createSpinner("Reading artifact"); - try { - const bytes = new Uint8Array(await readFile(targetFile)); - const identity = computeArtifactIdentity(bytes); - let artifactResult: PackageArtifactResponse | null = null; - - if (options.packageName?.trim()) { - const packageName = normalizePackageNameOrFail(options.packageName); - const registry = await getRegistry(opts, { cache: true }); - spinnerText(spinner, `Resolving ${packageName}`); - const targetVersion = await resolvePackageVersion(registry, packageName, { - version: options.version, - tag: options.tag, - }); - artifactResult = await apiRequestPackageArtifact(registry, packageName, targetVersion); - validateDownloadedArtifact(packageName, artifactResult, bytes, identity); - } - - const expectedSha256 = - options.sha256?.trim() || - (artifactResult?.artifact.kind === "npm-pack" ? artifactResult.artifact.sha256 : undefined); - const expectedNpmIntegrity = - options.npmIntegrity?.trim() || artifactResult?.artifact.npmIntegrity; - const expectedNpmShasum = options.npmShasum?.trim() || artifactResult?.artifact.npmShasum; - assertDigestMatch("SHA-256", expectedSha256, identity.sha256); - assertDigestMatch("npm integrity", expectedNpmIntegrity, identity.npmIntegrity); - assertDigestMatch("npm shasum", expectedNpmShasum, identity.npmShasum); - - spinner?.stop(); - const output = { - path: targetFile, - bytes: bytes.byteLength, - sha256: identity.sha256, - npmIntegrity: identity.npmIntegrity, - npmShasum: identity.npmShasum, - expected: { - sha256: expectedSha256, - npmIntegrity: expectedNpmIntegrity, - npmShasum: expectedNpmShasum, - package: artifactResult?.package.name, - version: artifactResult?.version, - artifactKind: artifactResult?.artifact.kind, - }, - verified: Boolean(expectedSha256 || expectedNpmIntegrity || expectedNpmShasum), - }; - if (options.json) { - process.stdout.write(`${JSON.stringify(output, null, 2)}\n`); - return; - } - console.log(`Path: ${targetFile}`); - console.log(`SHA-256: ${identity.sha256}`); - console.log(`npm integrity: ${identity.npmIntegrity}`); - console.log(`npm shasum: ${identity.npmShasum}`); - if (output.verified) { - console.log("OK. Artifact verification passed."); - } else { - console.log("Computed artifact digests. Pass --package or expected digests to verify."); - } - } catch (error) { - spinner?.fail(formatError(error)); - throw error; - } -} - -export async function cmdPackageReadiness( - opts: GlobalOpts, - packageName: string, - options: PackageReadinessOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - const registry = await getRegistry(opts, { cache: true }); - const result = await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/readiness`, - }, - ApiV1PackageReadinessResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - - console.log(`${result.package.name} readiness: ${result.ready ? "ready" : "blocked"}`); - for (const check of result.checks) { - console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`); - } - if (result.blockers.length > 0) { - console.log(`Blockers: ${result.blockers.join(", ")}`); - } -} - -export async function cmdPackageMigrationStatus( - opts: GlobalOpts, - packageName: string, - options: PackageMigrationStatusOptions = {}, -) { - const trimmed = normalizePackageNameOrFail(packageName); - const registry = await getRegistry(opts, { cache: true }); - const result = await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.packages}/${encodeURIComponent(trimmed)}/readiness`, - }, - ApiV1PackageReadinessResponseSchema, - ); - - if (options.json) { - process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); - return; - } - - const version = result.package.latestVersion ?? "no release"; - console.log(`${result.package.name} migration: ${result.ready ? "ready" : "blocked"}`); - console.log(`Version: ${version}`); - console.log(`Official: ${result.package.isOfficial ? "yes" : "no"}`); - for (const check of result.checks) { - console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`); - } - if (result.blockers.length > 0) { - console.log(`Blockers: ${result.blockers.join(", ")}`); - } -} - -async function apiRequestPackageDetail(registry: string, name: string) { - return await apiRequest( - registry, - { method: "GET", path: `${ApiRoutes.packages}/${encodeURIComponent(name)}` }, - ApiV1PackageResponseSchema, - ); -} - -async function apiRequestPackageArtifact( - registry: string, - name: string, - version: string, -) { - return await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}/artifact`, - }, - ApiV1PackageArtifactResponseSchema, - ); -} - -async function apiRequestPackageVersion( - registry: string, - name: string, - version: string, -) { - return await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/versions/${encodeURIComponent(version)}`, - }, - ApiV1PackageVersionResponseSchema, - ); -} - -async function apiRequestPackageVersions( - registry: string, - name: string, - limit: number, -) { - const url = registryUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/versions`, registry); - url.searchParams.set("limit", String(limit)); - return await apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1PackageVersionListResponseSchema, - ); -} - -async function resolvePackageVersion( - registry: string, - name: string, - args: { version?: string; tag?: string }, -) { - if (args.version?.trim()) return args.version.trim(); - const detail = await apiRequestPackageDetail(registry, name); - if (!detail.package) fail("Package not found"); - const tags = normalizeTags(detail.package.tags); - if (args.tag?.trim()) { - const tagged = tags[args.tag.trim()]; - if (!tagged) fail(`Unknown tag "${args.tag.trim()}"`); - return tagged; - } - const latest = detail.package.latestVersion ?? tags.latest; - if (!latest) fail("Could not resolve latest version"); - return latest; -} - -function normalizePackageNameOrFail(raw: string) { - const trimmed = raw.trim(); - if (!trimmed) fail("Package name required"); - return trimmed; -} - -function spinnerText(spinner: ReturnType | null, text: string) { - if (spinner) spinner.text = text; -} - -function clampLimit(value: number, max: number) { - if (!Number.isFinite(value)) return Math.min(25, max); - return Math.max(1, Math.min(Math.round(value), max)); -} - -function formatPackageLine(item: { - name: string; - displayName: string; - family: PackageFamily; - latestVersion?: string | null; - channel: "official" | "community" | "private"; - isOfficial: boolean; - verificationTier?: string | null; - summary?: string | null; -}) { - const flags = [ - familyLabel(item.family), - item.isOfficial ? "official" : item.channel, - item.verificationTier ?? null, - ].filter(Boolean); - const version = item.latestVersion ? ` v${item.latestVersion}` : ""; - const summary = item.summary ? ` ${item.summary}` : ""; - return `${item.name}${version} ${item.displayName} [${flags.join(", ")}]${summary}`; -} - -function computeArtifactIdentity(bytes: Uint8Array): ArtifactIdentity { - return { - sha256: digestHex(bytes, "sha256"), - npmIntegrity: `sha512-${digestBase64(bytes, "sha512")}`, - npmShasum: digestHex(bytes, "sha1"), - byteLength: bytes.byteLength, - }; -} - -function digestHex(bytes: Uint8Array, algorithm: "sha1" | "sha256") { - return createHash(algorithm).update(bytes).digest("hex"); -} - -function digestBase64(bytes: Uint8Array, algorithm: "sha512") { - return createHash(algorithm).update(bytes).digest("base64"); -} - -function validateDownloadedArtifact( - requestedPackageName: string, - artifactResult: PackageArtifactResponse, - bytes: Uint8Array, - identity: ArtifactIdentity, -) { - const artifact = artifactResult.artifact; - if (artifact.kind === "npm-pack") { - assertDigestMatch("SHA-256", artifact.sha256, identity.sha256); - if (typeof artifact.size === "number" && artifact.size !== identity.byteLength) { - fail(`artifact size mismatch: expected ${artifact.size}, got ${identity.byteLength}`); - } - assertDigestMatch("npm integrity", artifact.npmIntegrity, identity.npmIntegrity); - assertDigestMatch("npm shasum", artifact.npmShasum, identity.npmShasum); - const parsed = parseClawPack(bytes); - if (parsed.packageName !== artifactResult.package.name) { - fail( - `ClawPack package name mismatch: expected ${artifactResult.package.name}, got ${parsed.packageName}`, - ); - } - if (parsed.packageVersion !== artifactResult.version) { - fail( - `ClawPack package version mismatch: expected ${artifactResult.version}, got ${parsed.packageVersion}`, - ); - } - if (requestedPackageName !== artifactResult.package.name) { - fail( - `Resolved package mismatch: expected ${requestedPackageName}, got ${artifactResult.package.name}`, - ); - } - } - if (requestedPackageName !== artifactResult.package.name) { - fail( - `Resolved package mismatch: expected ${requestedPackageName}, got ${artifactResult.package.name}`, - ); - } -} - -function assertDigestMatch(label: string, expected: string | null | undefined, actual: string) { - if (!expected) return; - if (expected !== actual) { - fail(`${label} mismatch: expected ${expected}, got ${actual}`); - } -} - -function defaultArtifactFilename( - name: string, - version: string, - artifact: PackageArtifactResponse["artifact"], -) { - if (artifact.kind === "npm-pack" && artifact.npmTarballName) return artifact.npmTarballName; - const safeName = name - .replace(/^@/, "") - .replaceAll("/", "-") - .replace(/[^a-zA-Z0-9._-]/g, "-"); - return `${safeName}-${version}.${artifact.kind === "npm-pack" ? "tgz" : "zip"}`; -} - -async function resolveArtifactOutputPath( - opts: GlobalOpts, - output: string | undefined, - filename: string, -) { - if (!output?.trim()) return resolve(opts.workdir, filename); - const resolved = resolve(opts.workdir, output.trim()); - const outputStat = await stat(resolved).catch(() => null); - if (outputStat?.isDirectory()) return join(resolved, filename); - return resolved; -} - -async function assertOutputWritable(path: string, force: boolean) { - const existing = await stat(path).catch(() => null); - if (existing && !force) fail(`Refusing to overwrite ${path}. Use --force.`); - await mkdir(dirname(path), { recursive: true }); -} - -function printPackageSummary(detail: PackageResponse) { - if (!detail.package) return; - const pkg = detail.package; - console.log(`${pkg.name} ${pkg.displayName}`); - console.log(`Family: ${familyLabel(pkg.family)}`); - console.log(`Channel: ${pkg.channel}${pkg.isOfficial ? " (official)" : ""}`); - if (pkg.summary) console.log(`Summary: ${pkg.summary}`); - if (pkg.runtimeId) console.log(`Runtime ID: ${pkg.runtimeId}`); - if (detail.owner?.handle || detail.owner?.displayName) { - console.log(`Owner: ${detail.owner.handle ?? detail.owner.displayName}`); - } - console.log(`Created: ${formatTimestamp(pkg.createdAt)}`); - console.log(`Updated: ${formatTimestamp(pkg.updatedAt)}`); - if (pkg.latestVersion) console.log(`Latest: ${pkg.latestVersion}`); - printArtifact(pkg.artifact ?? null); - const tags = Object.entries(normalizeTags(pkg.tags)); - if (tags.length > 0) { - console.log(`Tags: ${tags.map(([tag, version]) => `${tag}=${version}`).join(", ")}`); - } -} - -function printVersionSummary(version: NonNullable) { - console.log(`Selected: ${version.version}`); - console.log(`Selected At: ${formatTimestamp(version.createdAt)}`); - if (version.changelog.trim()) console.log(`Changelog: ${truncate(version.changelog, 120)}`); -} - -function printCompatibility(compatibility: PackageCompatibility | null | undefined) { - if (!compatibility) return; - const entries = Object.entries(compatibility) - .filter(([, value]) => value !== undefined && value !== null) - .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(",") : String(value)}`); - if (entries.length > 0) console.log(`Compatibility: ${entries.join("; ")}`); -} - -function printCapabilities(capabilities: PackageCapabilitySummary | null | undefined) { - if (!capabilities) return; - console.log(`Executes code: ${capabilities.executesCode ? "yes" : "no"}`); - if (capabilities.pluginKind) console.log(`Plugin kind: ${capabilities.pluginKind}`); - if (capabilities.bundleFormat) console.log(`Bundle format: ${capabilities.bundleFormat}`); - if (capabilities.hostTargets?.length) { - console.log(`Host targets: ${capabilities.hostTargets.join(", ")}`); - } - if (capabilities.channels?.length) console.log(`Channels: ${capabilities.channels.join(", ")}`); - if (capabilities.providers?.length) { - console.log(`Providers: ${capabilities.providers.join(", ")}`); - } - if (capabilities.toolNames?.length) console.log(`Tools: ${capabilities.toolNames.join(", ")}`); - if (capabilities.commandNames?.length) { - console.log(`Commands: ${capabilities.commandNames.join(", ")}`); - } - if (capabilities.serviceNames?.length) { - console.log(`Services: ${capabilities.serviceNames.join(", ")}`); - } -} - -function printVerification(verification: PackageVerificationSummary | null | undefined) { - if (!verification) return; - console.log(`Verification: ${verification.tier} / ${verification.scope}`); - if (verification.summary) console.log(`Verification Summary: ${verification.summary}`); - if (verification.sourceRepo) console.log(`Source Repo: ${verification.sourceRepo}`); - if (verification.sourceCommit) console.log(`Source Commit: ${verification.sourceCommit}`); - if (verification.sourceTag) console.log(`Source Ref: ${verification.sourceTag}`); - if (verification.scanStatus) console.log(`Scan: ${verification.scanStatus}`); -} - -function printArtifact(artifact: PackageArtifactSummary | null | undefined) { - if (!artifact || typeof artifact !== "object") return; - const summary = artifact as { - kind?: string; - sha256?: string; - size?: number; - format?: string; - npmIntegrity?: string; - npmShasum?: string; - npmTarballName?: string; - }; - if (!summary.kind) return; - console.log(`Artifact: ${summary.kind}${summary.format ? ` (${summary.format})` : ""}`); - if (summary.sha256) console.log(`Artifact SHA-256: ${summary.sha256}`); - if (typeof summary.size === "number") { - console.log(`Artifact Size: ${formatByteCount(summary.size)}`); - } - if (summary.npmIntegrity) console.log(`npm integrity: ${summary.npmIntegrity}`); - if (summary.npmShasum) console.log(`npm shasum: ${summary.npmShasum}`); - if (summary.npmTarballName) console.log(`npm tarball: ${summary.npmTarballName}`); -} - -function normalizeTags(tags: unknown): Record { - if (!tags || typeof tags !== "object") return {}; - const resolved: Record = {}; - for (const [tag, version] of Object.entries(tags as Record)) { - if (typeof version === "string") resolved[tag] = version; - } - return resolved; -} - -function normalizeFiles(files: unknown): PrintableFile[] { - if (!Array.isArray(files)) return []; - return files - .map((file) => { - if (!file || typeof file !== "object") return null; - const entry = file as { - path?: unknown; - size?: unknown; - sha256?: unknown; - contentType?: unknown; - }; - if (typeof entry.path !== "string") return null; - return { - path: entry.path, - size: typeof entry.size === "number" ? entry.size : null, - sha256: typeof entry.sha256 === "string" ? entry.sha256 : null, - contentType: typeof entry.contentType === "string" ? entry.contentType : null, - }; - }) - .filter((entry): entry is PrintableFile => Boolean(entry)); -} - -function formatFileLine(file: PrintableFile) { - const size = typeof file.size === "number" ? `${file.size}B` : "?"; - const hash = file.sha256 ?? "?"; - return `- ${file.path} ${size} ${hash}`; -} - -function familyLabel(family: PackageFamily) { - switch (family) { - case "code-plugin": - return "Code Plugin"; - case "bundle-plugin": - return "Bundle Plugin"; - default: - return "Skill"; - } -} - -function truncate(value: string, max: number) { - return value.length <= max ? value : `${value.slice(0, max - 1)}…`; -} - -function formatTimestamp(value: number) { - return new Date(value).toISOString(); -} - -async function readJsonFile(path: string) { - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON.parse(raw) as unknown; - return parsed && typeof parsed === "object" && !Array.isArray(parsed) - ? (parsed as Record) - : null; - } catch { - return null; - } -} - -function packageJsonString(value: Record | null, key: string): string | undefined { - const candidate = value?.[key]; - return typeof candidate === "string" && candidate.trim() ? candidate.trim() : undefined; -} - -function assertClawPackSize(size: number, label: string) { - if (size > MAX_CLAWPACK_BYTES) { - fail(`ClawPack "${label}" exceeds 120MB limit`); - } -} - -function formatByteCount(value: number) { - if (value < 1024) return `${value} B`; - if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; - return `${(value / (1024 * 1024)).toFixed(1)} MB`; -} diff --git a/dt-skill/src/cli/commands/publish.test.ts b/dt-skill/src/cli/commands/publish.test.ts index 020b03ee..f6b66a6a 100644 --- a/dt-skill/src/cli/commands/publish.test.ts +++ b/dt-skill/src/cli/commands/publish.test.ts @@ -31,7 +31,7 @@ vi.mock("../prompts/search-multiselect.js", async () => { const { cmdPublish } = await import("./publish"); async function makeTmpWorkdir() { - const root = await mkdtemp(join(tmpdir(), "clawhub-publish-")); + const root = await mkdtemp(join(tmpdir(), "dt-skill-publish-")); return root; } @@ -249,7 +249,7 @@ describe("cmdPublish", () => { } }); - it('rejects plugin folders with guidance to use "clawhub package publish"', async () => { + it("rejects plugin folders with guidance to use a skill folder", async () => { const workdir = await makeTmpWorkdir(); try { const folder = join(workdir, "demo-plugin"); @@ -269,7 +269,7 @@ describe("cmdPublish", () => { tags: "latest", }), ).rejects.toThrow( - 'This looks like a plugin. Use "clawhub package publish " instead.', + "This folder looks like a code plugin, not a skill. Use a folder with SKILL.md.", ); expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); } finally { diff --git a/dt-skill/src/cli/commands/publish.ts b/dt-skill/src/cli/commands/publish.ts index 6b8a84f2..16c1ae48 100644 --- a/dt-skill/src/cli/commands/publish.ts +++ b/dt-skill/src/cli/commands/publish.ts @@ -35,7 +35,7 @@ export async function cmdPublish( ) { // Resolve folder path: try workdir first (standard behavior), // but fall back to cwd so relative paths work from whichever directory - // the user runs the command (workdir may point to a clawdbot workspace) + // the user runs the command. const folder = folderArg ? await resolveFolderPath(opts.workdir, folderArg) : null; @@ -43,7 +43,7 @@ export async function cmdPublish( const folderStat = await stat(folder).catch(() => null); if (!folderStat || !folderStat.isDirectory()) fail("Path must be a folder"); if (await looksLikePluginFolder(folder)) { - fail('This looks like a plugin. Use "clawhub package publish " instead.'); + fail("This folder looks like a code plugin, not a skill. Use a folder with SKILL.md."); } // Detect batch mode: if folder does NOT contain SKILL.md directly, diff --git a/dt-skill/src/cli/commands/sync.test.ts b/dt-skill/src/cli/commands/sync.test.ts deleted file mode 100644 index 426e806d..00000000 --- a/dt-skill/src/cli/commands/sync.test.ts +++ /dev/null @@ -1,538 +0,0 @@ -/* @vitest-environment node */ - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; - -const mockIntro = vi.fn(); -const mockOutro = vi.fn(); -const mockLog = vi.fn(); -const mockMultiselect = vi.fn(async (_args?: unknown) => [] as string[]); -let interactive = false; -const mocked = (value: T) => - value as T & { mockImplementation: (...args: unknown[]) => unknown }; - -const defaultFindSkillFolders = async (root: string) => { - if (!root.endsWith("/scan")) return []; - return [ - { folder: "/scan/new-skill", slug: "new-skill", displayName: "New Skill" }, - { folder: "/scan/synced-skill", slug: "synced-skill", displayName: "Synced Skill" }, - { folder: "/scan/update-skill", slug: "update-skill", displayName: "Update Skill" }, - ]; -}; - -vi.mock("@clack/prompts", () => ({ - intro: (value: string) => mockIntro(value), - outro: (value: string) => mockOutro(value), - multiselect: (args: unknown) => mockMultiselect(args), - text: vi.fn(async () => ""), - isCancel: () => false, -})); - -const registryMocks = createRegistryModuleMocks(); -const httpMocks = createHttpModuleMocks(); -const uiMocks = createUiModuleMocks(); -httpMocks.downloadZip.mockImplementation( - async (_registry?: unknown, _args?: unknown) => new Uint8Array([1, 2, 3]), -); -const mockApiRequest = httpMocks.apiRequest; -const mockFail = uiMocks.fail; -const mockSpinner = uiMocks.spinner; -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../ui.js", () => ({ - createSpinner: vi.fn(() => mockSpinner), - fail: (message: string) => mockFail(message), - formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), - isInteractive: () => interactive, - promptConfirm: uiMocks.promptConfirm, -})); - -vi.mock("../scanSkills.js", () => ({ - findSkillFolders: vi.fn(defaultFindSkillFolders), - getFallbackSkillRoots: vi.fn(() => []), -})); - -const mockResolveClawdbotSkillRoots = vi.fn( - async () => - ({ - roots: [] as string[], - labels: {} as Record, - }) as const, -); -vi.mock("../clawdbotConfig.js", () => ({ - resolveClawdbotSkillRoots: () => mockResolveClawdbotSkillRoots(), -})); - -const mockListTextFiles = vi.fn(async (folder: string) => [ - { relPath: "SKILL.md", bytes: new TextEncoder().encode(folder) }, -]); -const mockHashSkillFiles = vi.fn((files: Array<{ relPath: string; bytes: Uint8Array }>) => ({ - fingerprint: files - .map((file) => `${file.relPath}:${Buffer.from(file.bytes).toString("hex")}`) - .join("|"), - files: [], -})); -const mockHashSkillZip = vi.fn((_zip?: Uint8Array) => ({ - fingerprint: "remote-fingerprint", - files: [], -})); -const mockReadSkillOrigin = vi.fn(async (_folder?: string) => null); -vi.mock("../../skills.js", () => ({ - listTextFiles: (folder: string) => mockListTextFiles(folder), - hashSkillFiles: (files: Array<{ relPath: string; bytes: Uint8Array }>) => - mockHashSkillFiles(files), - hashSkillZip: (zip: Uint8Array) => mockHashSkillZip(zip), - readSkillOrigin: (folder: string) => mockReadSkillOrigin(folder), -})); - -const mockCmdPublish = vi.fn(); -vi.mock("./publish.js", () => ({ - cmdPublish: (opts: unknown, folder: unknown, options?: unknown) => - mockCmdPublish(opts, folder, options), -})); - -const { cmdSync } = await import("./sync"); - -function makeOpts() { - return makeGlobalOpts(); -} - -afterEach(async () => { - vi.clearAllMocks(); - mockCmdPublish.mockReset(); - process.exitCode = undefined; - const { findSkillFolders } = await import("../scanSkills.js"); - mocked(findSkillFolders).mockImplementation(defaultFindSkillFolders); -}); - -vi.spyOn(console, "log").mockImplementation((...args) => { - mockLog(args.map(String).join(" ")); -}); - -describe("cmdSync", () => { - it("classifies skills as new/update/synced (dry-run, mocked HTTP)", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: true }, true); - - expect(mockCmdPublish).not.toHaveBeenCalled(); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Already synced/); - expect(output).toMatch(/synced-skill/); - - const dryRunOutro = mockOutro.mock.calls.at(-1)?.[0]; - expect(String(dryRunOutro)).toMatch(/Dry run: would upload 2 skill/); - }); - - it("prints bullet lists and selects all actionable by default", async () => { - interactive = true; - mockMultiselect.mockImplementation(async (args?: unknown) => { - const { initialValues } = args as { initialValues: string[] }; - return initialValues; - }); - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: false, dryRun: false, bump: "patch" }, true); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/To sync/); - expect(output).toMatch(/- new-skill/); - expect(output).toMatch(/- update-skill/); - expect(output).toMatch(/Already synced/); - expect(output).toMatch(/- synced-skill/); - - const lastCall = mockMultiselect.mock.calls.at(-1); - const promptArgs = lastCall ? (lastCall[0] as { initialValues: string[] }) : undefined; - expect(promptArgs?.initialValues.length).toBe(2); - expect(mockCmdPublish).toHaveBeenCalledTimes(2); - }); - - it("labels unmatched local content as proposed publish versions, not registry updates", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: true }, true); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/update-skill\s+LOCAL CHANGES latest 1\.0\.0; publish 1\.0\.1/); - expect(output).toMatch(/new-skill\s+NEW \(publish 1\.0\.0\)/); - expect(output).not.toMatch(/UPDATE 1\.0\.0/); - }); - - it("shows condensed synced list when nothing to sync", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - return { match: { version: "1.0.0" }, latestVersion: { version: "1.0.0" } }; - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false }, true); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Already synced/); - expect(output).toMatch(/new-skill@1.0.0/); - expect(output).toMatch(/synced-skill@1.0.0/); - expect(output).not.toMatch(/\n-/); - - const outro = mockOutro.mock.calls.at(-1)?.[0]; - expect(String(outro)).toMatch(/Nothing to sync/); - }); - - it("dedupes duplicate slugs before publishing", async () => { - interactive = false; - const { findSkillFolders } = await import("../scanSkills.js"); - mocked(findSkillFolders).mockImplementation(async (root: string) => { - if (!root.endsWith("/scan")) return []; - return [ - { folder: "/scan/dup-skill", slug: "dup-skill", displayName: "Dup Skill" }, - { folder: "/scan/dup-skill-copy", slug: "dup-skill", displayName: "Dup Skill" }, - ]; - }); - - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - return { match: null, latestVersion: null }; - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false }, true); - - expect(mockCmdPublish).toHaveBeenCalledTimes(1); - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Skipped duplicate slugs/); - expect(output).toMatch(/dup-skill/); - }); - - it("prints labeled roots when clawdbot roots are detected", async () => { - interactive = false; - mockResolveClawdbotSkillRoots.mockResolvedValueOnce({ - roots: ["/auto"], - labels: { "/auto": "Agent: Work" }, - }); - const { findSkillFolders } = await import("../scanSkills.js"); - mocked(findSkillFolders).mockImplementation(async (root: string) => { - if (root === "/auto") { - return [{ folder: "/auto/alpha", slug: "alpha", displayName: "Alpha" }]; - } - return []; - }); - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - throw new Error("Skill not found"); - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { all: true, dryRun: true }, true); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Roots with skills/); - expect(output).toMatch(/Agent: Work/); - }); - - it("allows empty changelog for updates (interactive)", async () => { - interactive = true; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); - - const calls = mockCmdPublish.mock.calls.map( - (call) => call[2] as { slug: string; changelog: string }, - ); - const update = calls.find((c) => c.slug === "update-skill"); - if (!update) throw new Error("Missing update-skill publish"); - expect(update.changelog).toBe(""); - }); - - it("continues uploading after a publish failure", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - mockCmdPublish.mockImplementation(async (_opts, _folder, options?: unknown) => { - const { slug } = options as { slug: string }; - if (slug === "new-skill") { - throw new Error("Registry rejected upload"); - } - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); - - expect(mockCmdPublish).toHaveBeenCalledTimes(2); - expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ - "new-skill", - "update-skill", - ]); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Failed to upload/); - expect(output).toMatch(/new-skill/); - expect(output).toMatch(/Registry rejected upload/); - - const outro = mockOutro.mock.calls.at(-1)?.[0]; - expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); - expect(process.exitCode).toBe(1); - }); - - it("continues uploading after an alias slug conflict publish failure", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - mockCmdPublish.mockImplementation(async (_opts, _folder, options?: unknown) => { - const { slug } = options as { slug: string }; - if (slug === "new-skill") { - throw new Error( - "Slug redirects to an existing skill. Choose a different slug. Existing skill: /alice/demo", - ); - } - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); - - expect(mockCmdPublish).toHaveBeenCalledTimes(2); - expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ - "new-skill", - "update-skill", - ]); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Failed to upload/); - expect(output).toMatch(/Slug redirects to an existing skill/); - expect(output).toMatch(/Existing skill: \/alice\/demo/); - - const outro = mockOutro.mock.calls.at(-1)?.[0]; - expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); - expect(process.exitCode).toBe(1); - }); - - it("continues uploading after a locked slug publish failure", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - mockCmdPublish.mockImplementation(async (_opts, _folder, options?: unknown) => { - const { slug } = options as { slug: string }; - if (slug === "new-skill") { - throw new Error( - "This slug is locked to a deleted or banned account. If you believe you are the rightful owner, please contact security@openclaw.ai to reclaim it.", - ); - } - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); - - expect(mockCmdPublish).toHaveBeenCalledTimes(2); - expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ - "new-skill", - "update-skill", - ]); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Failed to upload/); - expect(output).toMatch(/This slug is locked to a deleted or banned account/); - - const outro = mockOutro.mock.calls.at(-1)?.[0]; - expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); - expect(process.exitCode).toBe(1); - }); - - it("records unrelated publish failures as per-skill failures", async () => { - interactive = false; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - const u = new URL(`https://x.test${args.path}`); - const slug = u.searchParams.get("slug"); - if (slug === "new-skill") { - throw new Error("Skill not found"); - } - if (slug === "synced-skill") { - return { match: { version: "1.2.3" }, latestVersion: { version: "1.2.3" } }; - } - if (slug === "update-skill") { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - mockCmdPublish.mockRejectedValueOnce(new Error("HTTP 500")); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: false, bump: "patch" }, true); - - expect(mockCmdPublish).toHaveBeenCalledTimes(2); - expect(mockCmdPublish.mock.calls.map((call) => (call[2] as { slug: string }).slug)).toEqual([ - "new-skill", - "update-skill", - ]); - - const output = mockLog.mock.calls.map((call) => String(call[0])).join("\n"); - expect(output).toMatch(/Failed to upload/); - expect(output).toMatch(/new-skill: HTTP 500/); - - const outro = mockOutro.mock.calls.at(-1)?.[0]; - expect(String(outro)).toMatch(/Uploaded 1 of 2 skill\(s\). 1 failed/); - expect(process.exitCode).toBe(1); - }); - - it("aborts command-level failures before publishing", async () => { - interactive = false; - const { findSkillFolders } = await import("../scanSkills.js"); - mocked(findSkillFolders).mockImplementation(async (root: string) => { - if (!root.endsWith("/scan")) return []; - return [{ folder: "/scan/update-skill", slug: "update-skill", displayName: "Update Skill" }]; - }); - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path === "/api/cli/telemetry/sync") return { ok: true }; - if (args.path.startsWith("/api/v1/resolve?")) { - return { match: null, latestVersion: { version: "1.0.0" } }; - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await expect( - cmdSync( - makeOpts(), - { root: ["/scan"], all: true, dryRun: false, bump: "not-semver" as never }, - true, - ), - ).rejects.toThrow("Could not bump version for update-skill"); - - expect(mockCmdPublish).not.toHaveBeenCalled(); - }); - - it("skips telemetry when CLAWHUB_DISABLE_TELEMETRY is set", async () => { - interactive = false; - process.env.CLAWHUB_DISABLE_TELEMETRY = "1"; - mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => { - if (args.path.startsWith("/api/v1/resolve?")) { - return { match: { version: "1.0.0" }, latestVersion: { version: "1.0.0" } }; - } - throw new Error(`Unexpected apiRequest: ${args.path}`); - }); - - await cmdSync(makeOpts(), { root: ["/scan"], all: true, dryRun: true }, true); - expect( - mockApiRequest.mock.calls.some((call) => call[1]?.path === "/api/cli/telemetry/sync"), - ).toBe(false); - delete process.env.CLAWHUB_DISABLE_TELEMETRY; - }); -}); diff --git a/dt-skill/src/cli/commands/sync.ts b/dt-skill/src/cli/commands/sync.ts deleted file mode 100644 index 7ae4311c..00000000 --- a/dt-skill/src/cli/commands/sync.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { intro, outro } from "@clack/prompts"; -import { hashSkillFiles, listTextFiles, readSkillOrigin } from "../../skills.js"; -import { resolveClawdbotSkillRoots } from "../clawdbotConfig.js"; -import { getFallbackSkillRoots } from "../scanSkills.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive } from "../ui.js"; -import { cmdPublish } from "./publish.js"; -import { - buildScanRoots, - checkRegistrySyncState, - dedupeSkillsBySlug, - formatActionableLine, - formatBulletList, - formatCommaList, - formatList, - formatSyncedDisplay, - formatSyncedSummary, - getRegistryForSync, - mapWithConcurrency, - mergeScan, - normalizeConcurrency, - printSection, - reportTelemetryIfEnabled, - resolvePublishMeta, - scanRootsWithLabels, - selectToUpload, -} from "./syncHelpers.js"; -import type { Candidate, LocalSkill, SyncOptions } from "./syncTypes.js"; - -export async function cmdSync(opts: GlobalOpts, options: SyncOptions, inputAllowed: boolean) { - const allowPrompt = isInteractive() && inputAllowed !== false; - intro("ClawHub sync"); - - const registry = await getRegistryForSync(opts); - const selectedRoots = buildScanRoots(opts, options.root); - const clawdbotRoots = await resolveClawdbotSkillRoots(); - const combinedRoots = Array.from( - new Set([...selectedRoots, ...clawdbotRoots.roots].map((root) => root.trim()).filter(Boolean)), - ); - const concurrency = normalizeConcurrency(options.concurrency); - - const spinner = createSpinner("Scanning for local skills"); - const primaryScan = await scanRootsWithLabels(combinedRoots, clawdbotRoots.labels); - let scan = primaryScan; - let telemetryScan = primaryScan; - if (primaryScan.skills.length === 0) { - const fallback = getFallbackSkillRoots(opts.workdir); - const fallbackScan = await scanRootsWithLabels(fallback); - spinner.stop(); - telemetryScan = mergeScan(primaryScan, fallbackScan); - scan = fallbackScan; - if (fallbackScan.skills.length === 0) - fail("No skills found (checked workdir and known Clawdis/Clawd locations)"); - printSection( - `No skills in workdir. Found ${fallbackScan.skills.length} in fallback locations.`, - formatList(fallbackScan.rootsWithSkills, 10), - ); - } else { - spinner.stop(); - const labeledRoots = primaryScan.rootsWithSkills - .map((root) => { - const label = primaryScan.rootLabels?.[root]; - return label ? `${label} (${root})` : root; - }) - .filter(Boolean); - if (labeledRoots.length > 0) { - printSection("Roots with skills", formatList(labeledRoots, 10)); - } - } - const deduped = dedupeSkillsBySlug(scan.skills); - const skills = deduped.skills; - if (deduped.duplicates.length > 0) { - printSection("Skipped duplicate slugs", formatCommaList(deduped.duplicates, 16)); - } - const parsingSpinner = createSpinner("Parsing local skills"); - const locals: LocalSkill[] = []; - try { - let done = 0; - const parsed = await mapWithConcurrency(skills, Math.min(concurrency, 12), async (skill) => { - const filesOnDisk = await listTextFiles(skill.folder); - const hashed = hashSkillFiles(filesOnDisk); - const origin = await readSkillOrigin(skill.folder); - done += 1; - parsingSpinner.text = `Parsing local skills ${done}/${skills.length}`; - return { - ...skill, - fingerprint: hashed.fingerprint, - fileCount: filesOnDisk.length, - origin, - }; - }); - locals.push(...parsed); - } catch (error) { - parsingSpinner.fail(formatError(error)); - throw error; - } finally { - parsingSpinner.stop(); - } - - const candidatesSpinner = createSpinner("Checking registry sync state"); - const candidates: Candidate[] = []; - const resolveSupport: { value: boolean | null } = { value: null }; - try { - let done = 0; - const resolved = await mapWithConcurrency(locals, Math.min(concurrency, 16), async (skill) => { - try { - return await checkRegistrySyncState(registry, skill, resolveSupport); - } finally { - done += 1; - candidatesSpinner.text = `Checking registry sync state ${done}/${locals.length}`; - } - }); - candidates.push(...resolved); - } catch (error) { - candidatesSpinner.fail(formatError(error)); - throw error; - } finally { - candidatesSpinner.stop(); - } - - await reportTelemetryIfEnabled({ - registry, - scan: telemetryScan, - candidates, - }); - - const synced = candidates.filter((candidate) => candidate.status === "synced"); - const actionable = candidates.filter((candidate) => candidate.status !== "synced"); - const bump = options.bump ?? "patch"; - - if (actionable.length === 0) { - if (synced.length > 0) { - printSection("Already synced", formatCommaList(synced.map(formatSyncedSummary), 16)); - } - outro("Nothing to sync."); - return; - } - - printSection( - "To sync", - formatBulletList( - actionable.map((candidate) => formatActionableLine(candidate, bump)), - 20, - ), - ); - if (synced.length > 0) { - printSection("Already synced", formatSyncedDisplay(synced)); - } - - const selected = await selectToUpload(actionable, { - allowPrompt, - all: Boolean(options.all), - bump, - }); - if (selected.length === 0) { - outro("Nothing selected."); - return; - } - - if (options.dryRun) { - outro(`Dry run: would upload ${selected.length} skill(s).`); - return; - } - - const tags = options.tags ?? "latest"; - const failedUploads: Array<{ slug: string; message: string }> = []; - let uploaded = 0; - - for (const skill of selected) { - const { publishVersion, changelog } = await resolvePublishMeta(skill, { - bump, - allowPrompt, - changelogFlag: options.changelog, - }); - const forkOf = - skill.origin && normalizeRegistry(skill.origin.registry) === normalizeRegistry(registry) - ? skill.origin.slug !== skill.slug - ? `${skill.origin.slug}@${skill.origin.installedVersion}` - : undefined - : undefined; - try { - await cmdPublish(opts, skill.folder, { - slug: skill.slug, - name: skill.displayName, - version: publishVersion, - changelog, - tags, - forkOf, - }); - uploaded += 1; - } catch (error) { - failedUploads.push({ slug: skill.slug, message: formatError(error) }); - } - } - - if (failedUploads.length > 0) { - printSection( - "Failed to upload", - formatBulletList( - failedUploads.map((failure) => `${failure.slug}: ${failure.message}`), - 20, - ), - ); - outro(`Uploaded ${uploaded} of ${selected.length} skill(s). ${failedUploads.length} failed.`); - process.exitCode = 1; - return; - } - - outro(`Uploaded ${selected.length} skill(s).`); -} - -function normalizeRegistry(value: string) { - return value.trim().replace(/\/+$/, "").toLowerCase(); -} diff --git a/dt-skill/src/cli/commands/syncHelpers.test.ts b/dt-skill/src/cli/commands/syncHelpers.test.ts deleted file mode 100644 index 794753cc..00000000 --- a/dt-skill/src/cli/commands/syncHelpers.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* @vitest-environment node */ -import { describe, expect, it, vi } from "vitest"; - -vi.mock("../scanSkills.js", () => ({ - findSkillFolders: vi.fn(async (root: string) => { - if (root.endsWith("/with-skill")) { - return [{ folder: `${root}/demo`, slug: "demo", displayName: "Demo" }]; - } - return []; - }), -})); - -const { scanRootsWithLabels } = await import("./syncHelpers.js"); - -describe("scanRootsWithLabels", () => { - it("attaches labels to roots with skills", async () => { - const roots = ["/tmp/with-skill", "/tmp/empty", "/tmp/with-skill"]; - const labels = { "/tmp/with-skill": "Agent: Work" }; - - const result = await scanRootsWithLabels(roots, labels); - - expect(result.rootsWithSkills).toEqual(["/tmp/with-skill"]); - expect(result.rootLabels).toEqual({ "/tmp/with-skill": "Agent: Work" }); - expect(result.skills.map((skill) => skill.slug)).toEqual(["demo"]); - }); -}); diff --git a/dt-skill/src/cli/commands/syncHelpers.ts b/dt-skill/src/cli/commands/syncHelpers.ts deleted file mode 100644 index 635bc6a1..00000000 --- a/dt-skill/src/cli/commands/syncHelpers.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { createHash } from "node:crypto"; -import { realpath } from "node:fs/promises"; -import { resolve } from "node:path"; -import { isCancel, multiselect } from "@clack/prompts"; -import semver from "semver"; -import { resolveHome } from "../../homedir.js"; -import { apiRequest, downloadZip, registryUrl } from "../../http.js"; -import { - ApiCliTelemetrySyncResponseSchema, - ApiRoutes, - ApiV1SkillResolveResponseSchema, - ApiV1SkillResponseSchema, - LegacyApiRoutes, -} from "../../schema/index.js"; -import { hashSkillZip } from "../../skills.js"; -import { getRegistry } from "../registry.js"; -import { findSkillFolders, type SkillFolder } from "../scanSkills.js"; -import type { GlobalOpts } from "../types.js"; -import { fail, formatError } from "../ui.js"; -import type { Candidate, LocalSkill } from "./syncTypes.js"; - -export async function reportTelemetryIfEnabled(params: { - registry: string; - scan: { roots: string[]; skillsByRoot: Record }; - candidates: Candidate[]; -}) { - if (isTelemetryDisabled()) return; - const versionBySlug = new Map(); - for (const candidate of params.candidates) { - versionBySlug.set(candidate.slug, candidate.matchVersion ?? null); - } - - const roots = params.scan.roots.map((root) => ({ - rootId: rootTelemetryId(root), - label: formatRootLabel(root), - skills: (params.scan.skillsByRoot[root] ?? []).map((skill) => ({ - slug: skill.slug, - version: versionBySlug.get(skill.slug) ?? null, - })), - })); - - try { - await apiRequest( - params.registry, - { - method: "POST", - path: LegacyApiRoutes.cliTelemetrySync, - body: { roots }, - }, - ApiCliTelemetrySyncResponseSchema, - ); - } catch { - // ignore telemetry failures - } -} - -function isTelemetryDisabled() { - const raw = process.env.CLAWHUB_DISABLE_TELEMETRY ?? process.env.CLAWDHUB_DISABLE_TELEMETRY; - if (!raw) return false; - return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase()); -} - -export function buildScanRoots(opts: GlobalOpts, extraRoots: string[] | undefined) { - const roots = [opts.workdir, opts.dir, ...(extraRoots ?? [])]; - return Array.from(new Set(roots.map((root) => resolve(root)))); -} - -export function normalizeConcurrency(value: number | undefined) { - const raw = typeof value === "number" ? value : 4; - const rounded = Number.isFinite(raw) ? Math.round(raw) : 4; - return Math.min(32, Math.max(1, rounded)); -} - -export async function mapWithConcurrency( - items: T[], - limit: number, - fn: (item: T) => Promise, -) { - const results = Array.from({ length: items.length }) as R[]; - let nextIndex = 0; - const workerCount = Math.min(Math.max(1, limit), items.length || 1); - - async function worker() { - while (true) { - const index = nextIndex; - nextIndex += 1; - if (index >= items.length) return; - results[index] = await fn(items[index] as T); - } - } - - await Promise.all(Array.from({ length: workerCount }, () => worker())); - return results; -} - -export async function checkRegistrySyncState( - registry: string, - skill: LocalSkill, - resolveSupport: { value: boolean | null }, -): Promise { - if (resolveSupport.value !== false) { - try { - const resolved = await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.resolve}?slug=${encodeURIComponent(skill.slug)}&hash=${encodeURIComponent(skill.fingerprint)}`, - }, - ApiV1SkillResolveResponseSchema, - ); - resolveSupport.value = true; - const latestVersion = resolved.latestVersion?.version ?? null; - const matchVersion = resolved.match?.version ?? null; - if (!latestVersion) { - return { - ...skill, - status: "new", - matchVersion: null, - latestVersion: null, - }; - } - return { - ...skill, - status: matchVersion ? "synced" : "update", - matchVersion, - latestVersion, - }; - } catch (error) { - const message = formatError(error); - if (/skill not found/i.test(message) || /HTTP 404/i.test(message)) { - resolveSupport.value = true; - return { - ...skill, - status: "new", - matchVersion: null, - latestVersion: null, - }; - } - if (/no matching routes found/i.test(message)) { - resolveSupport.value = false; - } else { - throw error; - } - } - } - - const meta = await apiRequest( - registry, - { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(skill.slug)}` }, - ApiV1SkillResponseSchema, - ).catch(() => null); - - const latestVersion = meta?.latestVersion?.version ?? null; - if (!latestVersion) { - return { - ...skill, - status: "new", - matchVersion: null, - latestVersion: null, - }; - } - - const zip = await downloadZip(registry, { - slug: skill.slug, - version: latestVersion, - }); - const remote = hashSkillZip(zip).fingerprint; - const matchVersion = remote === skill.fingerprint ? latestVersion : null; - - return { - ...skill, - status: matchVersion ? "synced" : "update", - matchVersion, - latestVersion, - }; -} - -export async function scanRootsWithLabels(roots: string[], labels?: Record) { - const all: SkillFolder[] = []; - const rootsWithSkills: string[] = []; - const uniqueRoots = await dedupeRoots(roots); - const skillsByRoot: Record = {}; - const rootLabels: Record = {}; - for (const root of uniqueRoots) { - const found = await findSkillFolders(root); - skillsByRoot[root] = found; - if (found.length > 0) rootsWithSkills.push(root); - all.push(...found); - if (labels?.[root]) rootLabels[root] = labels[root] as string; - } - const byFolder = new Map(); - for (const folder of all) { - byFolder.set(folder.folder, folder); - } - return { - roots: uniqueRoots, - skillsByRoot, - skills: Array.from(byFolder.values()), - rootsWithSkills, - rootLabels, - }; -} - -export function mergeScan( - left: { - roots: string[]; - skillsByRoot: Record; - skills: SkillFolder[]; - rootsWithSkills: string[]; - rootLabels: Record; - }, - right: { - roots: string[]; - skillsByRoot: Record; - skills: SkillFolder[]; - rootsWithSkills: string[]; - rootLabels: Record; - }, -) { - const mergedRoots = Array.from(new Set([...left.roots, ...right.roots])); - const skillsByRoot: Record = {}; - for (const root of mergedRoots) { - skillsByRoot[root] = right.skillsByRoot[root] ?? left.skillsByRoot[root] ?? []; - } - const rootLabels: Record = { ...left.rootLabels, ...right.rootLabels }; - const byFolder = new Map(); - for (const entry of [...left.skills, ...right.skills]) { - byFolder.set(entry.folder, entry); - } - const skills = Array.from(byFolder.values()); - const rootsWithSkills = mergedRoots.filter((root) => (skillsByRoot[root]?.length ?? 0) > 0); - return { roots: mergedRoots, skillsByRoot, skills, rootsWithSkills, rootLabels }; -} - -async function dedupeRoots(roots: string[]) { - const seen = new Set(); - const unique: string[] = []; - for (const root of roots) { - const resolved = resolve(root); - const canonical = await realpath(resolved).catch(() => null); - const key = canonical ?? resolved; - if (seen.has(key)) continue; - seen.add(key); - unique.push(key); - } - return unique; -} - -export async function selectToUpload( - candidates: Candidate[], - params: { allowPrompt: boolean; all: boolean; bump: "patch" | "minor" | "major" }, -): Promise { - if (params.all || !params.allowPrompt) return candidates; - - const valueByKey = new Map(); - const choices = candidates.map((candidate) => { - const key = candidate.folder; - valueByKey.set(key, candidate); - return { - value: key, - label: `${candidate.slug} ${formatActionableStatus(candidate, params.bump)}`, - hint: `${abbreviatePath(candidate.folder)} | ${candidate.fileCount} files`, - }; - }); - - const picked = await multiselect({ - message: "Select skills to upload", - options: choices, - initialValues: choices.map((choice) => choice.value), - required: false, - }); - if (isCancel(picked)) fail("Canceled"); - const selected = picked.map((key) => valueByKey.get(key)).filter(Boolean) as Candidate[]; - return selected; -} - -export async function resolvePublishMeta( - skill: Candidate, - params: { bump: "patch" | "minor" | "major"; allowPrompt: boolean; changelogFlag?: string }, -) { - if (skill.status === "new") { - return { publishVersion: "1.0.0", changelog: "" }; - } - - const latest = skill.latestVersion; - if (!latest) fail(`Could not resolve latest version for ${skill.slug}`); - const publishVersion = semver.inc(latest, params.bump); - if (!publishVersion) fail(`Could not bump version for ${skill.slug}`); - - const fromFlag = params.changelogFlag?.trim(); - if (fromFlag) return { publishVersion, changelog: fromFlag }; - - return { publishVersion, changelog: "" }; -} - -export async function getRegistryForSync(opts: GlobalOpts) { - return getRegistry(opts, { cache: true }); -} - -export function formatList(values: string[], max: number) { - if (values.length === 0) return ""; - const shown = values.map(abbreviatePath); - if (shown.length <= max) return shown.join("\n"); - const head = shown.slice(0, Math.max(1, max - 1)); - const rest = values.length - head.length; - return [...head, `… +${rest} more`].join("\n"); -} - -export function printSection(title: string, body?: string) { - const trimmed = body?.trim(); - if (!trimmed) { - console.log(title); - return; - } - if (trimmed.includes("\n")) { - console.log(`\n${title}\n${trimmed}`); - return; - } - console.log(`${title}: ${trimmed}`); -} - -function abbreviatePath(value: string) { - const home = resolveHome(); - if (value.startsWith(home)) return `~${value.slice(home.length)}`; - return value; -} - -function rootTelemetryId(value: string) { - return createHash("sha256").update(value).digest("hex"); -} - -function formatRootLabel(value: string) { - const home = resolveHome(); - if (value === home) return "~"; - - const normalized = value.replaceAll("\\", "/"); - const normalizedHome = home.replaceAll("\\", "/"); - const isHome = normalized === normalizedHome || normalized.startsWith(`${normalizedHome}/`); - - const stripped = isHome ? normalized.slice(normalizedHome.length).replace(/^\//, "") : normalized; - const parts = stripped.split("/").filter(Boolean); - const tail = parts.slice(-2).join("/"); - - if (!tail) return isHome ? "~" : "…"; - return isHome ? `~/${tail}` : `…/${tail}`; -} - -export function dedupeSkillsBySlug(skills: SkillFolder[]) { - const bySlug = new Map(); - for (const skill of skills) { - const existing = bySlug.get(skill.slug); - if (existing) existing.push(skill); - else bySlug.set(skill.slug, [skill]); - } - const unique: SkillFolder[] = []; - const duplicates: string[] = []; - for (const [slug, entries] of bySlug.entries()) { - unique.push(entries[0] as SkillFolder); - if (entries.length > 1) duplicates.push(`${slug} (${entries.length})`); - } - return { skills: unique, duplicates }; -} - -function formatActionableStatus(candidate: Candidate, bump: "patch" | "minor" | "major"): string { - if (candidate.status === "new") return "NEW (publish 1.0.0)"; - const latest = candidate.latestVersion; - const next = latest ? semver.inc(latest, bump) : null; - if (latest && next) return `LOCAL CHANGES latest ${latest}; publish ${next}`; - return "LOCAL CHANGES"; -} - -export function formatActionableLine( - candidate: Candidate, - bump: "patch" | "minor" | "major", -): string { - return `${candidate.slug} ${formatActionableStatus(candidate, bump)} (${candidate.fileCount} files)`; -} - -function formatSyncedLine(candidate: Candidate): string { - const version = candidate.matchVersion ?? candidate.latestVersion ?? "unknown"; - return `${candidate.slug} synced (${version})`; -} - -export function formatSyncedSummary(candidate: Candidate): string { - const version = candidate.matchVersion ?? candidate.latestVersion; - return version ? `${candidate.slug}@${version}` : candidate.slug; -} - -export function formatBulletList(lines: string[], max: number): string { - if (lines.length <= max) return lines.map((line) => `- ${line}`).join("\n"); - const head = lines.slice(0, max); - const rest = lines.length - head.length; - return [...head, `... +${rest} more`].map((line) => `- ${line}`).join("\n"); -} - -export function formatSyncedDisplay(synced: Candidate[]) { - const lines = synced.map(formatSyncedLine); - if (lines.length <= 12) return formatBulletList(lines, 12); - return formatCommaList(synced.map(formatSyncedSummary), 24); -} - -export function formatCommaList(values: string[], max: number) { - if (values.length === 0) return ""; - if (values.length <= max) return values.join(", "); - const head = values.slice(0, Math.max(1, max - 1)); - const rest = values.length - head.length; - return `${head.join(", ")}, ... +${rest} more`; -} diff --git a/dt-skill/src/cli/commands/syncTypes.ts b/dt-skill/src/cli/commands/syncTypes.ts deleted file mode 100644 index 86948a2d..00000000 --- a/dt-skill/src/cli/commands/syncTypes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { SkillOrigin } from "../../skills.js"; -import type { SkillFolder } from "../scanSkills.js"; - -export type SyncOptions = { - root?: string[]; - all?: boolean; - dryRun?: boolean; - bump?: "patch" | "minor" | "major"; - changelog?: string; - tags?: string; - concurrency?: number; -}; - -export type Candidate = SkillFolder & { - fingerprint: string; - fileCount: number; - origin: SkillOrigin | null; - status: "synced" | "new" | "update"; - matchVersion: string | null; - latestVersion: string | null; -}; - -export type LocalSkill = SkillFolder & { - fingerprint: string; - fileCount: number; - origin: SkillOrigin | null; -}; diff --git a/dt-skill/src/cli/scanSkills.test.ts b/dt-skill/src/cli/scanSkills.test.ts index 40a31e24..30e1b616 100644 --- a/dt-skill/src/cli/scanSkills.test.ts +++ b/dt-skill/src/cli/scanSkills.test.ts @@ -4,10 +4,10 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; -import { findSkillFolders, getFallbackSkillRoots } from "./scanSkills"; +import { findSkillFolders } from "./scanSkills"; async function makeTmpDir() { - return mkdtemp(join(tmpdir(), "clawhub-scan-")); + return mkdtemp(join(tmpdir(), "dt-skill-scan-")); } describe("scanSkills", () => { @@ -54,13 +54,4 @@ describe("scanSkills", () => { await rm(root, { recursive: true, force: true }); } }); - - it("includes known legacy roots", () => { - const roots = getFallbackSkillRoots("/tmp/anywhere"); - expect(roots.some((p) => p.endsWith("/clawdis/skills"))).toBe(true); - expect(roots.some((p) => p.endsWith("/clawd/skills"))).toBe(true); - expect(roots.some((p) => p.endsWith("/clawdbot/skills"))).toBe(true); - expect(roots.some((p) => p.endsWith("/openclaw/skills"))).toBe(true); - expect(roots.some((p) => p.endsWith("/moltbot/skills"))).toBe(true); - }); }); diff --git a/dt-skill/src/cli/scanSkills.ts b/dt-skill/src/cli/scanSkills.ts index 3bbf1c14..be022047 100644 --- a/dt-skill/src/cli/scanSkills.ts +++ b/dt-skill/src/cli/scanSkills.ts @@ -1,6 +1,5 @@ import { readdir, stat } from "node:fs/promises"; import { basename, join, resolve } from "node:path"; -import { resolveHome } from "../homedir.js"; import { sanitizeSlug, titleCase } from "./slug.js"; export type SkillFolder = { @@ -29,58 +28,6 @@ export async function findSkillFolders(root: string): Promise { return results.sort((a, b) => a.slug.localeCompare(b.slug)); } -export function getFallbackSkillRoots(workdir: string) { - const home = resolveHome(); - const roots = [ - // adjacent repo installs - resolve(workdir, "..", "clawdis", "skills"), - resolve(workdir, "..", "clawdis", "Skills"), - resolve(workdir, "..", "clawdbot", "skills"), - resolve(workdir, "..", "clawdbot", "Skills"), - resolve(workdir, "..", "openclaw", "skills"), - resolve(workdir, "..", "openclaw", "Skills"), - resolve(workdir, "..", "moltbot", "skills"), - resolve(workdir, "..", "moltbot", "Skills"), - - // legacy locations - resolve(home, "clawd", "skills"), - resolve(home, "clawd", "Skills"), - resolve(home, ".clawd", "skills"), - resolve(home, ".clawd", "Skills"), - - resolve(home, "clawdbot", "skills"), - resolve(home, "clawdbot", "Skills"), - resolve(home, ".clawdbot", "skills"), - resolve(home, ".clawdbot", "Skills"), - - resolve(home, "clawdis", "skills"), - resolve(home, "clawdis", "Skills"), - resolve(home, ".clawdis", "skills"), - resolve(home, ".clawdis", "Skills"), - - resolve(home, "openclaw", "skills"), - resolve(home, "openclaw", "Skills"), - resolve(home, ".openclaw", "skills"), - resolve(home, ".openclaw", "Skills"), - - resolve(home, "moltbot", "skills"), - resolve(home, "moltbot", "Skills"), - resolve(home, ".moltbot", "skills"), - resolve(home, ".moltbot", "Skills"), - - // macOS App Support legacy - resolve(home, "Library", "Application Support", "clawdbot", "skills"), - resolve(home, "Library", "Application Support", "clawdbot", "Skills"), - resolve(home, "Library", "Application Support", "clawdis", "skills"), - resolve(home, "Library", "Application Support", "clawdis", "Skills"), - resolve(home, "Library", "Application Support", "openclaw", "skills"), - resolve(home, "Library", "Application Support", "openclaw", "Skills"), - resolve(home, "Library", "Application Support", "moltbot", "skills"), - resolve(home, "Library", "Application Support", "moltbot", "Skills"), - ]; - return Array.from(new Set(roots)); -} - async function isSkillFolder(folder: string): Promise { const marker = await findSkillMarker(folder); if (!marker) return null; diff --git a/dt-skill/src/schema/index.ts b/dt-skill/src/schema/index.ts index 4c2ae22e..6b992d52 100644 --- a/dt-skill/src/schema/index.ts +++ b/dt-skill/src/schema/index.ts @@ -2,8 +2,6 @@ export type { ArkValidator } from "./ark.js"; export { parseArk } from "./ark.js"; export * from "./clawScanNote.js"; export { PLATFORM_SKILL_LICENSE, PLATFORM_SKILL_LICENSE_SUMMARY } from "./license.js"; -export * from "./openclawContract.js"; -export * from "./packages.js"; export { ApiRoutes, LegacyApiRoutes } from "./routes.js"; export * from "./schemas.js"; export * from "./textFiles.js"; diff --git a/dt-skill/src/schema/openclawContract.ts b/dt-skill/src/schema/openclawContract.ts deleted file mode 100644 index 126d2098..00000000 --- a/dt-skill/src/schema/openclawContract.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { PackageCompatibility } from "./packages.js"; - -type JsonObject = Record; - -export type OpenClawExternalPluginValidationIssue = { - fieldPath: string; - message: string; -}; - -export type OpenClawExternalCodePluginValidation = { - compatibility?: PackageCompatibility; - issues: OpenClawExternalPluginValidationIssue[]; -}; - -export const OPENCLAW_EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [ - "openclaw.compat.pluginApi", - "openclaw.build.openclawVersion", -] as const; -const COMPILED_RUNTIME_EXTENSIONS = [".js", ".mjs", ".cjs"] as const; - -function isRecord(value: unknown): value is JsonObject { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - -function getTrimmedString(value: unknown): string | undefined { - return typeof value === "string" && value.trim() ? value.trim() : undefined; -} - -function getTrimmedStringArray(value: unknown): string[] { - return Array.isArray(value) - ? value - .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) - .map((entry) => entry.trim()) - : []; -} - -function normalizePackagePath(value: string): string { - return value.trim().replaceAll("\\", "/").replace(/^\.\//, ""); -} - -function isTypeScriptRuntimeEntry(value: string): boolean { - return /\.(?:c|m)?ts$/u.test(value); -} - -function compiledRuntimeCandidates(entry: string): string[] { - const normalized = normalizePackagePath(entry); - const withoutExtension = normalized.replace(/\.[^.]+$/u, ""); - const distBase = normalized.startsWith("src/") - ? `dist/${normalized.slice("src/".length).replace(/\.[^.]+$/u, "")}` - : `dist/${withoutExtension}`; - return [ - ...COMPILED_RUNTIME_EXTENSIONS.map((ext) => `${distBase}${ext}`), - ...COMPILED_RUNTIME_EXTENSIONS.map((ext) => `${withoutExtension}${ext}`), - ]; -} - -function readOpenClawBlock(packageJson: unknown) { - const root = isRecord(packageJson) ? packageJson : undefined; - const openclaw = isRecord(root?.openclaw) ? root.openclaw : undefined; - const compat = isRecord(openclaw?.compat) ? openclaw.compat : undefined; - const build = isRecord(openclaw?.build) ? openclaw.build : undefined; - const install = isRecord(openclaw?.install) ? openclaw.install : undefined; - return { root, openclaw, compat, build, install }; -} - -export function normalizeOpenClawExternalPluginCompatibility( - packageJson: unknown, -): PackageCompatibility | undefined { - const { root, compat, build, install } = readOpenClawBlock(packageJson); - const version = getTrimmedString(root?.version); - const minHostVersion = getTrimmedString(install?.minHostVersion); - const compatibility: PackageCompatibility = {}; - - const pluginApi = getTrimmedString(compat?.pluginApi); - if (pluginApi) { - compatibility.pluginApiRange = pluginApi; - } - - const minGatewayVersion = getTrimmedString(compat?.minGatewayVersion) ?? minHostVersion; - if (minGatewayVersion) { - compatibility.minGatewayVersion = minGatewayVersion; - } - - const builtWithOpenClawVersion = getTrimmedString(build?.openclawVersion) ?? version; - if (builtWithOpenClawVersion) { - compatibility.builtWithOpenClawVersion = builtWithOpenClawVersion; - } - - const pluginSdkVersion = getTrimmedString(build?.pluginSdkVersion); - if (pluginSdkVersion) { - compatibility.pluginSdkVersion = pluginSdkVersion; - } - - return Object.keys(compatibility).length > 0 ? compatibility : undefined; -} - -export function listMissingOpenClawExternalCodePluginFieldPaths(packageJson: unknown): string[] { - const { compat, build } = readOpenClawBlock(packageJson); - const missing: string[] = []; - if (!getTrimmedString(compat?.pluginApi)) { - missing.push("openclaw.compat.pluginApi"); - } - if (!getTrimmedString(build?.openclawVersion)) { - missing.push("openclaw.build.openclawVersion"); - } - return missing; -} - -export function validateOpenClawExternalCodePluginPackageJson( - packageJson: unknown, -): OpenClawExternalCodePluginValidation { - const issues = listMissingOpenClawExternalCodePluginFieldPaths(packageJson).map((fieldPath) => ({ - fieldPath, - message: `${fieldPath} is required for external code plugins published to ClawHub.`, - })); - return { - compatibility: normalizeOpenClawExternalPluginCompatibility(packageJson), - issues, - }; -} - -export function validateOpenClawExternalCodePluginPackageContents( - packageJson: unknown, - filePaths: Iterable, -): OpenClawExternalCodePluginValidation { - const validation = validateOpenClawExternalCodePluginPackageJson(packageJson); - const { root, openclaw } = readOpenClawBlock(packageJson); - const name = getTrimmedString(root?.name) ?? "package"; - const packageFiles = new Set(Array.from(filePaths, normalizePackagePath)); - const sourceEntries = getTrimmedStringArray(openclaw?.extensions); - const runtimeEntries = getTrimmedStringArray(openclaw?.runtimeExtensions); - - if (runtimeEntries.length > 0 && runtimeEntries.length !== sourceEntries.length) { - validation.issues.push({ - fieldPath: "openclaw.runtimeExtensions", - message: `${name} openclaw.runtimeExtensions length (${runtimeEntries.length}) must match openclaw.extensions length (${sourceEntries.length}).`, - }); - } - - for (const runtimeEntry of runtimeEntries) { - const normalized = normalizePackagePath(runtimeEntry); - if (!packageFiles.has(normalized)) { - validation.issues.push({ - fieldPath: "openclaw.runtimeExtensions", - message: `${name} runtime extension entry not found: ./${normalized}`, - }); - } - } - - if (runtimeEntries.length === 0) { - for (const sourceEntry of sourceEntries) { - if (!isTypeScriptRuntimeEntry(sourceEntry)) continue; - const candidates = compiledRuntimeCandidates(sourceEntry); - if (candidates.some((candidate) => packageFiles.has(candidate))) continue; - validation.issues.push({ - fieldPath: "openclaw.extensions", - message: `${name} requires compiled runtime output for TypeScript entry ${sourceEntry}: expected ${candidates.map((candidate) => `./${candidate}`).join(", ")}`, - }); - } - } - - return validation; -} diff --git a/dt-skill/src/schema/packages.ts b/dt-skill/src/schema/packages.ts deleted file mode 100644 index 97611840..00000000 --- a/dt-skill/src/schema/packages.ts +++ /dev/null @@ -1,738 +0,0 @@ -import { type inferred, type } from "arktype"; -import { CliPublishFileSchema, PublishSourceSchema } from "./schemas.js"; - -export const PackageFamilySchema = type('"skill"|"code-plugin"|"bundle-plugin"'); -export type PackageFamily = (typeof PackageFamilySchema)[inferred]; - -export const PackageChannelSchema = type('"official"|"community"|"private"'); -export type PackageChannel = (typeof PackageChannelSchema)[inferred]; - -export const PackageVerificationTierSchema = type( - '"structural"|"source-linked"|"provenance-verified"|"rebuild-verified"', -); -export type PackageVerificationTier = (typeof PackageVerificationTierSchema)[inferred]; - -export const PackageVerificationScopeSchema = type('"artifact-only"|"dependency-graph-aware"'); -export type PackageVerificationScope = (typeof PackageVerificationScopeSchema)[inferred]; - -export const PackageCompatibilitySchema = type({ - pluginApiRange: "string?", - builtWithOpenClawVersion: "string?", - pluginSdkVersion: "string?", - minGatewayVersion: "string?", -}); -export type PackageCompatibility = (typeof PackageCompatibilitySchema)[inferred]; - -export const PackageCapabilitySummarySchema = type({ - executesCode: "boolean", - runtimeId: "string?", - pluginKind: "string?", - channels: "string[]?", - providers: "string[]?", - hooks: "string[]?", - bundledSkills: "string[]?", - setupEntry: "boolean?", - configSchema: "boolean?", - configUiHints: "boolean?", - materializesDependencies: "boolean?", - toolNames: "string[]?", - commandNames: "string[]?", - serviceNames: "string[]?", - capabilityTags: "string[]?", - httpRouteCount: "number?", - bundleFormat: "string?", - hostTargets: "string[]?", -}); -export type PackageCapabilitySummary = (typeof PackageCapabilitySummarySchema)[inferred]; - -export const PackageVerificationSummarySchema = type({ - tier: PackageVerificationTierSchema, - scope: PackageVerificationScopeSchema, - summary: "string?", - sourceRepo: "string?", - sourceCommit: "string?", - sourceTag: "string?", - hasProvenance: "boolean?", - scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', -}); -export type PackageVerificationSummary = (typeof PackageVerificationSummarySchema)[inferred]; - -export const PackageStatsSchema = type({ - downloads: "number", - installs: "number", - stars: "number", - versions: "number", -}); -export type PackageStats = (typeof PackageStatsSchema)[inferred]; - -export const PackageArtifactKindSchema = type('"legacy-zip"|"npm-pack"'); -export type PackageArtifactKind = (typeof PackageArtifactKindSchema)[inferred]; - -export const PackageReleaseModerationStateSchema = type('"approved"|"quarantined"|"revoked"'); -export type PackageReleaseModerationState = (typeof PackageReleaseModerationStateSchema)[inferred]; - -export const PackageReportStatusSchema = type('"open"|"confirmed"|"dismissed"'); -export type PackageReportStatus = (typeof PackageReportStatusSchema)[inferred]; -export const PackageReportFinalActionSchema = type('"none"|"quarantine"|"revoke"'); -export type PackageReportFinalAction = (typeof PackageReportFinalActionSchema)[inferred]; - -export const PackageReportListStatusSchema = PackageReportStatusSchema.or('"all"'); -export type PackageReportListStatus = (typeof PackageReportListStatusSchema)[inferred]; - -export const PackageAppealStatusSchema = type('"open"|"accepted"|"rejected"'); -export type PackageAppealStatus = (typeof PackageAppealStatusSchema)[inferred]; -export const PackageAppealFinalActionSchema = type('"none"|"approve"'); -export type PackageAppealFinalAction = (typeof PackageAppealFinalActionSchema)[inferred]; - -export const PackageAppealListStatusSchema = PackageAppealStatusSchema.or('"all"'); -export type PackageAppealListStatus = (typeof PackageAppealListStatusSchema)[inferred]; - -export const PackageOfficialMigrationPhaseSchema = type( - '"planned"|"published"|"clawpack-ready"|"legacy-zip-only"|"metadata-ready"|"blocked"|"ready-for-openclaw"', -); -export type PackageOfficialMigrationPhase = (typeof PackageOfficialMigrationPhaseSchema)[inferred]; - -export const PackageOfficialMigrationListPhaseSchema = - PackageOfficialMigrationPhaseSchema.or('"all"'); -export type PackageOfficialMigrationListPhase = - (typeof PackageOfficialMigrationListPhaseSchema)[inferred]; - -export const PackageArtifactSummarySchema = type({ - kind: PackageArtifactKindSchema, - sha256: "string?", - size: "number?", - format: "string?", - npmIntegrity: "string?", - npmShasum: "string?", - npmTarballName: "string?", - npmUnpackedSize: "number?", - npmFileCount: "number?", - source: '"clawhub"?', - artifactKind: PackageArtifactKindSchema.optional(), - artifactSha256: "string?", - packageName: "string?", - version: "string?", -}); -export type PackageArtifactSummary = (typeof PackageArtifactSummarySchema)[inferred]; - -export const PackagePublishArtifactSchema = type({ - kind: '"npm-pack"', - storageId: "string", - sha256: "string", - size: "number", - format: '"tgz"', - npmIntegrity: "string", - npmShasum: "string", - npmTarballName: "string", - npmUnpackedSize: "number", - npmFileCount: "number", -}); -export type PackagePublishArtifact = (typeof PackagePublishArtifactSchema)[inferred]; - -export const PackageVtAnalysisSchema = type({ - status: "string", - verdict: "string?", - analysis: "string?", - source: "string?", - checkedAt: "number", -}); -export type PackageVtAnalysis = (typeof PackageVtAnalysisSchema)[inferred]; - -export const PackageLlmAnalysisDimensionSchema = type({ - name: "string", - label: "string", - rating: "string", - detail: "string", -}); -export type PackageLlmAnalysisDimension = (typeof PackageLlmAnalysisDimensionSchema)[inferred]; - -export const PackageLlmAnalysisSchema = type({ - status: "string", - verdict: "string?", - confidence: "string?", - summary: "string?", - dimensions: PackageLlmAnalysisDimensionSchema.array().optional(), - guidance: "string?", - findings: "string?", - agenticRiskFindings: "unknown[]?", - riskSummary: "unknown?", - model: "string?", - checkedAt: "number", -}); -export type PackageLlmAnalysis = (typeof PackageLlmAnalysisSchema)[inferred]; - -export const PackageStaticFindingSchema = type({ - code: "string", - severity: "string", - file: "string", - line: "number", - message: "string", - evidence: "string", -}); -export type PackageStaticFinding = (typeof PackageStaticFindingSchema)[inferred]; - -export const PackageStaticScanSchema = type({ - status: "string", - reasonCodes: "string[]", - findings: PackageStaticFindingSchema.array(), - summary: "string", - engineVersion: "string", - checkedAt: "number", -}); -export type PackageStaticScan = (typeof PackageStaticScanSchema)[inferred]; - -export const BundlePublishMetadataSchema = type({ - id: "string?", - format: "string?", - hostTargets: "string[]?", -}); -export type BundlePublishMetadata = (typeof BundlePublishMetadataSchema)[inferred]; - -export const PackageTrustedPublisherSchema = type({ - provider: '"github-actions"', - repository: "string", - repositoryId: "string", - repositoryOwner: "string", - repositoryOwnerId: "string", - workflowFilename: "string", - environment: "string?", -}); -export type PackageTrustedPublisher = (typeof PackageTrustedPublisherSchema)[inferred]; - -export const PackagePublishRequestSchema = type({ - name: "string", - displayName: "string?", - ownerHandle: "string?", - family: PackageFamilySchema, - version: "string", - changelog: "string", - clawScanNote: "string?", - manualOverrideReason: "string?", - channel: PackageChannelSchema.optional(), - tags: "string[]?", - source: PublishSourceSchema.optional(), - bundle: BundlePublishMetadataSchema.optional(), - artifact: PackagePublishArtifactSchema.optional(), - files: CliPublishFileSchema.array(), -}); -export type PackagePublishRequest = (typeof PackagePublishRequestSchema)[inferred]; - -export const PackageListItemSchema = type({ - name: "string", - displayName: "string", - family: PackageFamilySchema, - runtimeId: "string|null?", - channel: PackageChannelSchema, - isOfficial: "boolean", - summary: "string|null?", - ownerHandle: "string|null?", - createdAt: "number", - updatedAt: "number", - latestVersion: "string|null?", - capabilityTags: "string[]?", - executesCode: "boolean?", - verificationTier: PackageVerificationTierSchema.or("null").optional(), -}); -export type PackageListItem = (typeof PackageListItemSchema)[inferred]; - -export const ApiV1PackageListResponseSchema = type({ - items: PackageListItemSchema.array(), - nextCursor: "string|null", -}); - -export const ApiV1PackageSearchResponseSchema = type({ - results: type({ - score: "number", - package: PackageListItemSchema, - }).array(), -}); - -export const ApiV1PackageResponseSchema = type({ - package: type({ - name: "string", - displayName: "string", - family: PackageFamilySchema, - runtimeId: "string|null?", - channel: PackageChannelSchema, - isOfficial: "boolean", - summary: "string|null?", - ownerHandle: "string|null?", - createdAt: "number", - updatedAt: "number", - latestVersion: "string|null?", - tags: "unknown", - compatibility: PackageCompatibilitySchema.or("null").optional(), - capabilities: PackageCapabilitySummarySchema.or("null").optional(), - verification: PackageVerificationSummarySchema.or("null").optional(), - artifact: PackageArtifactSummarySchema.or("null").optional(), - stats: PackageStatsSchema.optional(), - }).or("null"), - owner: type({ - handle: "string|null", - displayName: "string|null?", - image: "string|null?", - }).or("null"), -}); - -export const ApiV1PackageVersionListResponseSchema = type({ - items: type({ - version: "string", - createdAt: "number", - changelog: "string", - distTags: "string[]?", - }).array(), - nextCursor: "string|null", -}); - -export const ApiV1PackageVersionResponseSchema = type({ - package: type({ - name: "string", - displayName: "string", - family: PackageFamilySchema, - }).or("null"), - version: type({ - version: "string", - createdAt: "number", - changelog: "string", - distTags: "string[]?", - files: "unknown", - compatibility: PackageCompatibilitySchema.or("null").optional(), - capabilities: PackageCapabilitySummarySchema.or("null").optional(), - verification: PackageVerificationSummarySchema.or("null").optional(), - artifact: PackageArtifactSummarySchema.or("null").optional(), - sha256hash: "string|null?", - vtAnalysis: PackageVtAnalysisSchema.or("null").optional(), - llmAnalysis: PackageLlmAnalysisSchema.or("null").optional(), - clawScanNote: "string|null?", - clawScanNoteUpdatedAt: "number|null?", - staticScan: PackageStaticScanSchema.or("null").optional(), - }).or("null"), -}); - -export const ApiV1PackageArtifactResponseSchema = type({ - package: type({ - name: "string", - displayName: "string", - family: PackageFamilySchema, - }), - version: "string", - artifact: type({ - kind: PackageArtifactKindSchema, - sha256: "string?", - size: "number?", - format: "string?", - npmIntegrity: "string?", - npmShasum: "string?", - npmTarballName: "string?", - npmUnpackedSize: "number?", - npmFileCount: "number?", - downloadUrl: "string", - tarballUrl: "string?", - legacyDownloadUrl: "string?", - source: '"clawhub"?', - artifactKind: PackageArtifactKindSchema.optional(), - artifactSha256: "string?", - packageName: "string?", - version: "string?", - }), -}); -export type ApiV1PackageArtifactResponse = (typeof ApiV1PackageArtifactResponseSchema)[inferred]; - -export const ApiV1PackageSecurityResponseSchema = type({ - package: type({ - name: "string", - displayName: "string", - family: PackageFamilySchema, - }), - release: type({ - releaseId: "string", - version: "string", - artifactKind: PackageArtifactKindSchema.or("null").optional(), - artifactSha256: "string?", - npmIntegrity: "string?", - npmShasum: "string?", - npmTarballName: "string?", - createdAt: "number", - }), - trust: type({ - scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', - moderationState: PackageReleaseModerationStateSchema.or("null").optional(), - blockedFromDownload: "boolean", - reasons: "string[]", - pending: "boolean", - stale: "boolean", - }), -}); -export type ApiV1PackageSecurityResponse = (typeof ApiV1PackageSecurityResponseSchema)[inferred]; - -export const PackageReleaseModerationRequestSchema = type({ - state: PackageReleaseModerationStateSchema, - reason: "string", -}); -export type PackageReleaseModerationRequest = - (typeof PackageReleaseModerationRequestSchema)[inferred]; - -export const PackageReportRequestSchema = type({ - reason: "string", - version: "string?", -}); -export type PackageReportRequest = (typeof PackageReportRequestSchema)[inferred]; - -export const ApiV1PackageReportResponseSchema = type({ - ok: "true", - reported: "boolean", - alreadyReported: "boolean", - packageId: "string", - releaseId: "string|null", - reportCount: "number", -}); -export type ApiV1PackageReportResponse = (typeof ApiV1PackageReportResponseSchema)[inferred]; - -export const PackageReportTriageRequestSchema = type({ - status: PackageReportStatusSchema, - note: "string?", - finalAction: PackageReportFinalActionSchema.optional(), -}); -export type PackageReportTriageRequest = (typeof PackageReportTriageRequestSchema)[inferred]; - -export const PackageAppealRequestSchema = type({ - version: "string", - message: "string", -}); -export type PackageAppealRequest = (typeof PackageAppealRequestSchema)[inferred]; - -export const ApiV1PackageAppealResponseSchema = type({ - ok: "true", - submitted: "boolean", - alreadyOpen: "boolean", - appealId: "string", - packageId: "string", - releaseId: "string", - status: PackageAppealStatusSchema, -}); -export type ApiV1PackageAppealResponse = (typeof ApiV1PackageAppealResponseSchema)[inferred]; - -export const PackageAppealResolveRequestSchema = type({ - status: PackageAppealStatusSchema, - note: "string?", - finalAction: PackageAppealFinalActionSchema.optional(), -}); -export type PackageAppealResolveRequest = (typeof PackageAppealResolveRequestSchema)[inferred]; - -export const ApiV1PackageAppealListResponseSchema = type({ - items: type({ - appealId: "string", - packageId: "string", - releaseId: "string", - name: "string", - displayName: "string", - family: PackageFamilySchema, - version: "string", - message: "string", - status: PackageAppealStatusSchema, - createdAt: "number", - submitter: type({ - userId: "string", - handle: "string|null?", - displayName: "string|null?", - }), - resolvedAt: "number|null?", - resolvedBy: "string|null?", - resolutionNote: "string|null?", - actionTaken: PackageAppealFinalActionSchema.or("null").optional(), - }).array(), - nextCursor: "string|null", - done: "boolean", -}); -export type ApiV1PackageAppealListResponse = - (typeof ApiV1PackageAppealListResponseSchema)[inferred]; - -export const ApiV1PackageAppealResolveResponseSchema = type({ - ok: "true", - appealId: "string", - packageId: "string", - releaseId: "string", - status: PackageAppealStatusSchema, - actionTaken: PackageAppealFinalActionSchema.optional(), -}); -export type ApiV1PackageAppealResolveResponse = - (typeof ApiV1PackageAppealResolveResponseSchema)[inferred]; - -export const ApiV1PackageReportListResponseSchema = type({ - items: type({ - reportId: "string", - packageId: "string", - releaseId: "string|null?", - name: "string", - displayName: "string", - family: PackageFamilySchema, - version: "string|null?", - reason: "string|null?", - status: PackageReportStatusSchema, - createdAt: "number", - reporter: type({ - userId: "string", - handle: "string|null?", - displayName: "string|null?", - }), - triagedAt: "number|null?", - triagedBy: "string|null?", - triageNote: "string|null?", - actionTaken: PackageReportFinalActionSchema.or("null").optional(), - }).array(), - nextCursor: "string|null", - done: "boolean", -}); -export type ApiV1PackageReportListResponse = - (typeof ApiV1PackageReportListResponseSchema)[inferred]; - -export const ApiV1PackageReportTriageResponseSchema = type({ - ok: "true", - reportId: "string", - packageId: "string", - status: PackageReportStatusSchema, - reportCount: "number", - actionTaken: PackageReportFinalActionSchema.optional(), -}); -export type ApiV1PackageReportTriageResponse = - (typeof ApiV1PackageReportTriageResponseSchema)[inferred]; - -export const ApiV1PackageModerationStatusResponseSchema = type({ - package: type({ - packageId: "string", - name: "string", - displayName: "string", - family: PackageFamilySchema, - channel: PackageChannelSchema, - isOfficial: "boolean", - reportCount: "number", - lastReportedAt: "number|null?", - scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"?', - }), - latestRelease: type({ - releaseId: "string", - version: "string", - artifactKind: PackageArtifactKindSchema.or("null").optional(), - scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', - moderationState: PackageReleaseModerationStateSchema.or("null").optional(), - moderationReason: "string|null?", - blockedFromDownload: "boolean", - reasons: "string[]", - createdAt: "number", - }).or("null"), -}); -export type ApiV1PackageModerationStatusResponse = - (typeof ApiV1PackageModerationStatusResponseSchema)[inferred]; - -export const PackageArtifactBackfillRequestSchema = type({ - cursor: "string|null?", - batchSize: "number?", - dryRun: "boolean?", -}); -export type PackageArtifactBackfillRequest = - (typeof PackageArtifactBackfillRequestSchema)[inferred]; - -export const ApiV1PackageArtifactBackfillResponseSchema = type({ - ok: "true", - scanned: "number", - updated: "number", - nextCursor: "string|null", - done: "boolean", - dryRun: "boolean", -}); -export type ApiV1PackageArtifactBackfillResponse = - (typeof ApiV1PackageArtifactBackfillResponseSchema)[inferred]; - -export const PackageReadinessCheckSchema = type({ - id: "string", - label: "string", - status: '"pass"|"warn"|"fail"', - message: "string", -}); -export type PackageReadinessCheck = (typeof PackageReadinessCheckSchema)[inferred]; - -export const ApiV1PackageReadinessResponseSchema = type({ - package: type({ - name: "string", - displayName: "string", - family: PackageFamilySchema, - isOfficial: "boolean", - latestVersion: "string|null?", - }), - ready: "boolean", - checks: PackageReadinessCheckSchema.array(), - blockers: "string[]", -}); -export type ApiV1PackageReadinessResponse = (typeof ApiV1PackageReadinessResponseSchema)[inferred]; - -export const PackageTransferRequestSchema = type({ - toOwner: "string", - reason: "string?", -}); -export type PackageTransferRequest = (typeof PackageTransferRequestSchema)[inferred]; - -export const ApiV1PackageTransferResponseSchema = type({ - ok: "true", - packageId: "string", - name: "string", - ownerUserId: "string", - ownerPublisherId: "string?", - channel: PackageChannelSchema, - isOfficial: "boolean", -}); -export type ApiV1PackageTransferResponse = (typeof ApiV1PackageTransferResponseSchema)[inferred]; - -export const PackageRepairNameRequestSchema = type({ - nextName: "string", - retireTarget: "boolean?", - owner: "string?", - reason: "string", - dryRun: "boolean?", -}); -export type PackageRepairNameRequest = (typeof PackageRepairNameRequestSchema)[inferred]; - -export const PackageRepairNamePackageSchema = type({ - packageId: "string", - name: "string", - runtimeId: "string|null?", - ownerUserId: "string", - ownerPublisherId: "string|null?", - channel: PackageChannelSchema, - softDeletedAt: "number|null?", -}); -export type PackageRepairNamePackage = (typeof PackageRepairNamePackageSchema)[inferred]; - -export const PackageRepairNameOperationSchema = type({ - action: '"retire-target"|"rename-source"|"transfer-owner"', - packageId: "string?", - from: "string?", - to: "string?", - owner: "string?", -}); -export type PackageRepairNameOperation = (typeof PackageRepairNameOperationSchema)[inferred]; - -export const ApiV1PackageRepairNameResponseSchema = type({ - ok: "true", - dryRun: "boolean", - source: PackageRepairNamePackageSchema, - target: PackageRepairNamePackageSchema.or("null"), - retiredName: "string|null?", - operations: PackageRepairNameOperationSchema.array(), -}); -export type ApiV1PackageRepairNameResponse = - (typeof ApiV1PackageRepairNameResponseSchema)[inferred]; - -export const PackageOfficialMigrationUpsertRequestSchema = type({ - bundledPluginId: "string", - packageName: "string", - owner: "string?", - sourceRepo: "string?", - sourcePath: "string?", - sourceCommit: "string?", - phase: PackageOfficialMigrationPhaseSchema.optional(), - blockers: "string[]?", - hostTargetsComplete: "boolean?", - scanClean: "boolean?", - moderationApproved: "boolean?", - runtimeBundlesReady: "boolean?", - notes: "string?", -}); -export type PackageOfficialMigrationUpsertRequest = - (typeof PackageOfficialMigrationUpsertRequestSchema)[inferred]; - -export const PackageOfficialMigrationItemSchema = type({ - migrationId: "string", - bundledPluginId: "string", - packageName: "string", - packageId: "string|null?", - owner: "string|null?", - sourceRepo: "string|null?", - sourcePath: "string|null?", - sourceCommit: "string|null?", - phase: PackageOfficialMigrationPhaseSchema, - blockers: "string[]", - hostTargetsComplete: "boolean", - scanClean: "boolean", - moderationApproved: "boolean", - runtimeBundlesReady: "boolean", - notes: "string|null?", - createdAt: "number", - updatedAt: "number", -}); -export type PackageOfficialMigrationItem = (typeof PackageOfficialMigrationItemSchema)[inferred]; - -export const ApiV1PackageOfficialMigrationListResponseSchema = type({ - items: PackageOfficialMigrationItemSchema.array(), - nextCursor: "string|null", - done: "boolean", -}); -export type ApiV1PackageOfficialMigrationListResponse = - (typeof ApiV1PackageOfficialMigrationListResponseSchema)[inferred]; - -export const ApiV1PackageOfficialMigrationResponseSchema = type({ - ok: "true", - migration: PackageOfficialMigrationItemSchema, -}); -export type ApiV1PackageOfficialMigrationResponse = - (typeof ApiV1PackageOfficialMigrationResponseSchema)[inferred]; - -export const PackageModerationQueueStatusSchema = type('"open"|"blocked"|"manual"|"all"'); -export type PackageModerationQueueStatus = (typeof PackageModerationQueueStatusSchema)[inferred]; - -export const ApiV1PackageModerationQueueResponseSchema = type({ - items: type({ - packageId: "string", - releaseId: "string", - name: "string", - displayName: "string", - family: PackageFamilySchema, - channel: PackageChannelSchema, - isOfficial: "boolean", - version: "string", - createdAt: "number", - artifactKind: PackageArtifactKindSchema.or("null").optional(), - scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', - moderationState: PackageReleaseModerationStateSchema.or("null").optional(), - moderationReason: "string|null?", - sourceRepo: "string|null?", - sourceCommit: "string|null?", - reportCount: "number", - lastReportedAt: "number|null?", - reasons: "string[]", - }).array(), - nextCursor: "string|null", - done: "boolean", -}); -export type ApiV1PackageModerationQueueResponse = - (typeof ApiV1PackageModerationQueueResponseSchema)[inferred]; - -export const ApiV1PackageReleaseModerationResponseSchema = type({ - ok: "true", - packageId: "string", - releaseId: "string", - state: PackageReleaseModerationStateSchema, - scanStatus: '"clean"|"malicious"', -}); -export type ApiV1PackageReleaseModerationResponse = - (typeof ApiV1PackageReleaseModerationResponseSchema)[inferred]; - -export const ApiV1PackagePublishResponseSchema = type({ - ok: "true", - packageId: "string", - releaseId: "string", -}); -export type ApiV1PackagePublishResponse = (typeof ApiV1PackagePublishResponseSchema)[inferred]; - -export const PackageTrustedPublisherUpsertRequestSchema = type({ - repository: "string", - workflowFilename: "string", - environment: "string?", -}); -export type PackageTrustedPublisherUpsertRequest = - (typeof PackageTrustedPublisherUpsertRequestSchema)[inferred]; - -export const ApiV1PackageTrustedPublisherResponseSchema = type({ - trustedPublisher: PackageTrustedPublisherSchema.or("null"), -}); -export type ApiV1PackageTrustedPublisherResponse = - (typeof ApiV1PackageTrustedPublisherResponseSchema)[inferred]; diff --git a/dt-skill/src/schema/routes.ts b/dt-skill/src/schema/routes.ts index 74e5d81d..31544c8f 100644 --- a/dt-skill/src/schema/routes.ts +++ b/dt-skill/src/schema/routes.ts @@ -15,9 +15,6 @@ export const ApiRoutes = { resolve: "/api/v1/resolve", download: "/api/v1/download", skills: "/api/v1/skills", - packages: "/api/v1/packages", - codePlugins: "/api/v1/code-plugins", - bundlePlugins: "/api/v1/bundle-plugins", stars: "/api/v1/stars", transfers: "/api/v1/transfers", publishers: "/api/v1/publishers", diff --git a/dt-skill/test-artifact/cli.artifact.test.ts b/dt-skill/test-artifact/cli.artifact.test.ts index 72136e88..7c25b0fc 100644 --- a/dt-skill/test-artifact/cli.artifact.test.ts +++ b/dt-skill/test-artifact/cli.artifact.test.ts @@ -1,7 +1,7 @@ /* @vitest-environment node */ import { spawnSync } from 'node:child_process'; -import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -28,17 +28,6 @@ function runNode(args: string[], envOverrides: NodeJS.ProcessEnv = {}) { }); } -function runGit(cwd: string, args: string[]) { - const result = spawnSync('git', ['-C', cwd, ...args], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - if (result.status !== 0) { - throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`); - } - return result.stdout.trim(); -} - afterEach(async () => { while (tempDirs.length > 0) { await rm(tempDirs.pop()!, { recursive: true, force: true }); @@ -55,7 +44,7 @@ describe('built CLI artifact', () => { }); it('loads the fingerprint contract from the built package', async () => { - const isolatedPackage = await makeTmpDir('clawhub-artifact-package-'); + const isolatedPackage = await makeTmpDir('dt-skill-artifact-package-'); await cp(join(packageRoot, 'bin'), join(isolatedPackage, 'bin'), { recursive: true }); await cp(join(packageRoot, 'dist'), join(isolatedPackage, 'dist'), { recursive: true }); await cp(join(packageRoot, 'package.json'), join(isolatedPackage, 'package.json')); @@ -74,78 +63,6 @@ describe('built CLI artifact', () => { expect(result.stderr).toBe(''); }); - it('packs a local code plugin in json mode from built output', async () => { - const root = await makeTmpDir('clawhub-artifact-'); - const pluginDir = join(root, 'demo-plugin'); - const packDestination = join(root, 'packs'); - await mkdir(join(pluginDir, 'src'), { recursive: true }); - await writeFile( - join(pluginDir, 'package.json'), - JSON.stringify({ - name: '@openclaw/demo-plugin', - displayName: 'Demo Plugin', - version: '1.0.0', - openclaw: { - compat: { - pluginApi: '>=2026.3.24-beta.2', - minGatewayVersion: '2026.3.24-beta.2', - }, - build: { - openclawVersion: '2026.3.24-beta.2', - pluginSdkVersion: '2026.3.24-beta.2', - }, - }, - }), - 'utf8' - ); - await writeFile( - join(pluginDir, 'openclaw.plugin.json'), - JSON.stringify({ - id: 'demo.plugin', - configSchema: { - type: 'object', - additionalProperties: false, - }, - }), - 'utf8' - ); - await writeFile(join(pluginDir, 'src', 'index.ts'), 'export const demo = true;\n', 'utf8'); - - runGit(root, ['init']); - runGit(root, ['remote', 'add', 'origin', 'https://github.com/openclaw/demo-plugin.git']); - runGit(root, ['add', '.']); - runGit(root, [ - '-c', - 'user.name=Test', - '-c', - 'user.email=test@example.com', - 'commit', - '-m', - 'init', - ]); - - const result = runNode( - [ - binPath, - 'package', - 'pack', - pluginDir, - '--json', - '--pack-destination', - packDestination, - ], - { NPM_CONFIG_CACHE: join(root, '.npm-cache') } - ); - - expect(result.status, result.stderr || result.stdout).toBe(0); - expect(result.stderr).toBe(''); - const output = JSON.parse(result.stdout.trim()) as Record; - expect(output.name).toBe('@openclaw/demo-plugin'); - expect(output.version).toBe('1.0.0'); - expect(output.path).toBeTypeOf('string'); - expect(output.sha256).toBeTypeOf('string'); - }); - it('keeps the built dist free of compiled test files', async () => { expect(dirname(distCliPath)).toBe(join(packageRoot, 'dist')); const result = runNode([ diff --git a/dt-skill/vitest.config.ts b/dt-skill/vitest.config.ts index 250df383..04b54e23 100644 --- a/dt-skill/vitest.config.ts +++ b/dt-skill/vitest.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ testTimeout: 15_000, hookTimeout: 15_000, include: ["src/**/*.test.ts"], - exclude: ["dist/**", "node_modules/**", "test-artifact/**", "src/cli/commands/packages.test.ts"], + exclude: ["dist/**", "node_modules/**", "test-artifact/**"], }, }); From 8d9567f86e2037e5d129ea2c0623b15867f068f9 Mon Sep 17 00:00:00 2001 From: huaiju Date: Tue, 16 Jun 2026 11:05:49 +0800 Subject: [PATCH 10/13] refactor: rename ClawHub protocol to dt-skill and skillsRegistry Align registry discovery, env vars, and local state with dt-skill naming and rename backend clawhub modules to skillsRegistry. Co-authored-by: Cursor --- .../{clawhub.js => skillsRegistry.js} | 32 ++++---- app/router.js | 30 ++++---- app/service/{clawhub.js => skillsRegistry.js} | 6 +- config/config.default.js | 2 +- .../skill-fingerprint/golden-vectors.v1.json | 2 +- contracts/skill-fingerprint/index.js | 3 +- dt-skill/package.json | 10 +-- dt-skill/src/cli/buildInfo.ts | 3 +- dt-skill/src/cli/commands/github.test.ts | 2 +- dt-skill/src/cli/commands/github.ts | 4 +- .../src/cli/commands/skills.install.test.ts | 10 +-- dt-skill/src/cli/commands/skills.test.ts | 10 +-- dt-skill/src/cli/commands/skills.ts | 6 +- dt-skill/src/cli/registry.test.ts | 20 +++-- dt-skill/src/cli/registry.ts | 22 +----- dt-skill/src/cli/ui.test.ts | 8 +- dt-skill/src/config.test.ts | 6 +- dt-skill/src/config.ts | 14 +--- dt-skill/src/discovery.ts | 32 ++++---- dt-skill/src/http.bun.test.ts | 16 ++-- dt-skill/src/http.test.ts | 4 +- dt-skill/src/http.ts | 6 +- dt-skill/src/schema/schemas.test.ts | 2 +- dt-skill/src/schema/textFiles.test.ts | 2 +- dt-skill/src/skills.test.ts | 60 ++++++++------- dt-skill/src/skills.ts | 69 +++++++---------- dt-skill/test/cliCommandTestKit.ts | 6 +- ...st.js => skills-registry-contract.test.js} | 74 +++++++++---------- ...js => skills-registry-integration.test.js} | 10 +-- 29 files changed, 216 insertions(+), 255 deletions(-) rename app/controller/{clawhub.js => skillsRegistry.js} (78%) rename app/service/{clawhub.js => skillsRegistry.js} (99%) rename test/{clawhub-contract.test.js => skills-registry-contract.test.js} (91%) rename test/{clawhub-integration.test.js => skills-registry-integration.test.js} (94%) diff --git a/app/controller/clawhub.js b/app/controller/skillsRegistry.js similarity index 78% rename from app/controller/clawhub.js rename to app/controller/skillsRegistry.js index 25c4d6e5..63454f53 100644 --- a/app/controller/clawhub.js +++ b/app/controller/skillsRegistry.js @@ -1,10 +1,10 @@ const Controller = require('egg').Controller; -class ClawhubController extends Controller { - // GET /.well-known/clawhub.json +class SkillsRegistryController extends Controller { + // GET /.well-known/dt-skill.json async registryMetadata() { const { ctx } = this; - const data = await ctx.service.clawhub.getRegistryMetadata(ctx.origin); + const data = await ctx.service.skillsRegistry.getRegistryMetadata(ctx.origin); ctx.body = data; } @@ -12,7 +12,7 @@ class ClawhubController extends Controller { async search() { const { ctx } = this; const { q, limit } = ctx.query; - const results = await ctx.service.clawhub.searchSkills(q, limit); + const results = await ctx.service.skillsRegistry.searchSkills(q, limit); ctx.body = { results }; } @@ -20,7 +20,7 @@ class ClawhubController extends Controller { async list() { const { ctx } = this; const { cursor, sort, limit } = ctx.query; - const data = await ctx.service.clawhub.listSkills(cursor, sort, limit); + const data = await ctx.service.skillsRegistry.listSkills(cursor, sort, limit); ctx.body = data; } @@ -28,7 +28,7 @@ class ClawhubController extends Controller { async detail() { const { ctx } = this; const { slug } = ctx.params; - const data = await ctx.service.clawhub.getSkillDetail(slug); + const data = await ctx.service.skillsRegistry.getSkillDetail(slug); if (!data) { ctx.status = 404; ctx.body = { error: '技能不存在' }; @@ -41,7 +41,7 @@ class ClawhubController extends Controller { async versions() { const { ctx } = this; const { slug } = ctx.params; - const data = await ctx.service.clawhub.listSkillVersions(slug); + const data = await ctx.service.skillsRegistry.listSkillVersions(slug); if (!data) { ctx.status = 404; ctx.body = { error: '技能不存在' }; @@ -54,7 +54,7 @@ class ClawhubController extends Controller { async versionDetail() { const { ctx } = this; const { slug, version } = ctx.params; - const data = await ctx.service.clawhub.getSkillVersionDetail(slug, version); + const data = await ctx.service.skillsRegistry.getSkillVersionDetail(slug, version); if (!data) { ctx.status = 404; ctx.body = { error: '版本不存在' }; @@ -68,7 +68,7 @@ class ClawhubController extends Controller { const { ctx } = this; const { slug } = ctx.params; const { path: filePath } = ctx.query; - const data = await ctx.service.clawhub.getSkillFileContent(slug, filePath); + const data = await ctx.service.skillsRegistry.getSkillFileContent(slug, filePath); if (!data) { ctx.status = 404; ctx.body = { error: '文件不存在' }; @@ -81,7 +81,7 @@ class ClawhubController extends Controller { async download() { const { ctx } = this; const { slug } = ctx.query; - const result = await ctx.service.clawhub.buildSkillZip(slug); + const result = await ctx.service.skillsRegistry.buildSkillZip(slug); if (!result) { ctx.status = 404; ctx.body = { error: '技能不存在' }; @@ -99,7 +99,7 @@ class ClawhubController extends Controller { async resolve() { const { ctx } = this; const { slug, hash } = ctx.query; - const data = await ctx.service.clawhub.resolveFingerprint(slug, hash); + const data = await ctx.service.skillsRegistry.resolveFingerprint(slug, hash); if (!data) { ctx.status = 404; ctx.body = { error: '未找到匹配的技能' }; @@ -129,13 +129,13 @@ class ClawhubController extends Controller { } } - const result = await ctx.service.clawhub.publishSkill(parsedPayload, files); + const result = await ctx.service.skillsRegistry.publishSkill(parsedPayload, files); ctx.body = result; } finally { try { await ctx.cleanupRequestFiles(); } catch (err) { - ctx.logger.warn('[clawhub] 清理临时上传文件失败:', err); + ctx.logger.warn('[skillsRegistry] 清理临时上传文件失败:', err); } } } @@ -144,7 +144,7 @@ class ClawhubController extends Controller { async delete() { const { ctx } = this; const { slug } = ctx.params; - const result = await ctx.service.clawhub.deleteSkill(slug); + const result = await ctx.service.skillsRegistry.deleteSkill(slug); ctx.body = result; } @@ -152,7 +152,7 @@ class ClawhubController extends Controller { async undelete() { const { ctx } = this; const { slug } = ctx.params; - const result = await ctx.service.clawhub.undeleteSkill(slug); + const result = await ctx.service.skillsRegistry.undeleteSkill(slug); ctx.body = result; } @@ -181,4 +181,4 @@ class ClawhubController extends Controller { } } -module.exports = ClawhubController; +module.exports = SkillsRegistryController; diff --git a/app/router.js b/app/router.js index 9e3ca8d1..6c3eb575 100644 --- a/app/router.js +++ b/app/router.js @@ -163,28 +163,28 @@ module.exports = (app) => { app.get('/api/skills/like-status', app.controller.skillLike.getLikeStatus); /** - * Clawhub Registry API (v1) + * Skills Registry API (v1) */ const skillsStorageReady = app.middleware.skillsStorageReady(); - app.get('/.well-known/clawhub.json', app.controller.clawhub.registryMetadata); - app.get('/api/v1/search', skillsStorageReady, app.controller.clawhub.search); - app.get('/api/v1/skills', skillsStorageReady, app.controller.clawhub.list); - app.get('/api/v1/skills/:slug', skillsStorageReady, app.controller.clawhub.detail); - app.get('/api/v1/skills/:slug/versions', skillsStorageReady, app.controller.clawhub.versions); + app.get('/.well-known/dt-skill.json', app.controller.skillsRegistry.registryMetadata); + app.get('/api/v1/search', skillsStorageReady, app.controller.skillsRegistry.search); + app.get('/api/v1/skills', skillsStorageReady, app.controller.skillsRegistry.list); + app.get('/api/v1/skills/:slug', skillsStorageReady, app.controller.skillsRegistry.detail); + app.get('/api/v1/skills/:slug/versions', skillsStorageReady, app.controller.skillsRegistry.versions); app.get( '/api/v1/skills/:slug/versions/:version', skillsStorageReady, - app.controller.clawhub.versionDetail + app.controller.skillsRegistry.versionDetail ); - app.get('/api/v1/skills/:slug/file', skillsStorageReady, app.controller.clawhub.fileContent); - app.get('/api/v1/download', skillsStorageReady, app.controller.clawhub.download); - app.get('/api/v1/resolve', skillsStorageReady, app.controller.clawhub.resolve); - app.post('/api/v1/skills', skillsStorageReady, app.controller.clawhub.publish); - app.delete('/api/v1/skills/:slug', skillsStorageReady, app.controller.clawhub.delete); - app.post('/api/v1/skills/:slug/undelete', skillsStorageReady, app.controller.clawhub.undelete); - app.post('/api/v1/stars/:slug', skillsStorageReady, app.controller.clawhub.star); - app.delete('/api/v1/stars/:slug', skillsStorageReady, app.controller.clawhub.unstar); + app.get('/api/v1/skills/:slug/file', skillsStorageReady, app.controller.skillsRegistry.fileContent); + app.get('/api/v1/download', skillsStorageReady, app.controller.skillsRegistry.download); + app.get('/api/v1/resolve', skillsStorageReady, app.controller.skillsRegistry.resolve); + app.post('/api/v1/skills', skillsStorageReady, app.controller.skillsRegistry.publish); + app.delete('/api/v1/skills/:slug', skillsStorageReady, app.controller.skillsRegistry.delete); + app.post('/api/v1/skills/:slug/undelete', skillsStorageReady, app.controller.skillsRegistry.undelete); + app.post('/api/v1/stars/:slug', skillsStorageReady, app.controller.skillsRegistry.star); + app.delete('/api/v1/stars/:slug', skillsStorageReady, app.controller.skillsRegistry.unstar); // io.of('/').route('getShellCommand', io.controller.home.getShellCommand) // 暂时close Terminal相关功能 diff --git a/app/service/clawhub.js b/app/service/skillsRegistry.js similarity index 99% rename from app/service/clawhub.js rename to app/service/skillsRegistry.js index 77f3007d..63ceae07 100644 --- a/app/service/clawhub.js +++ b/app/service/skillsRegistry.js @@ -9,7 +9,7 @@ const skillFingerprint = require('../../contracts/skill-fingerprint'); const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[\w.-]+)?(?:\+[\w.-]+)?$/; const SKILL_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -class ClawhubService extends Service { +class SkillsRegistryService extends Service { // Well-Known Registry Metadata async getRegistryMetadata(origin) { return { @@ -376,7 +376,7 @@ class ClawhubService extends Service { } } } catch (err) { - this.ctx.logger.error(`[clawhub] 读取上传文件 ${originalName} 失败:`, err); + this.ctx.logger.error(`[skillsRegistry] 读取上传文件 ${originalName} 失败:`, err); } processedFiles.push({ filename: originalName, @@ -610,4 +610,4 @@ class ClawhubService extends Service { } } -module.exports = ClawhubService; +module.exports = SkillsRegistryService; diff --git a/config/config.default.js b/config/config.default.js index d016f70f..b659705a 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -70,7 +70,7 @@ module.exports = (app) => { cron: '0 30 4 * * *', // 每天4:30清理 }, whitelist: (filename) => { - // 允许所有类型的文件上传(Clawhub 技能包含各种代码/配置文件与说明文档) + // 允许所有类型的文件上传(Skills Registry 技能包含各种代码/配置文件与说明文档) return true; }, }; diff --git a/contracts/skill-fingerprint/golden-vectors.v1.json b/contracts/skill-fingerprint/golden-vectors.v1.json index 4cdee77c..05798966 100644 --- a/contracts/skill-fingerprint/golden-vectors.v1.json +++ b/contracts/skill-fingerprint/golden-vectors.v1.json @@ -26,7 +26,7 @@ "name": "stored-ignore-files", "ignoreFiles": [ { "path": ".gitignore", "content": "ignored.md\nbuild/\n" }, - { "path": ".clawhubignore", "content": "private.txt\n" } + { "path": ".dt-skillignore", "content": "private.txt\n" } ], "files": [ { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, diff --git a/contracts/skill-fingerprint/index.js b/contracts/skill-fingerprint/index.js index 23a03522..35de34e2 100644 --- a/contracts/skill-fingerprint/index.js +++ b/contracts/skill-fingerprint/index.js @@ -9,8 +9,7 @@ const TEXT_FILE_EXTENSION_SET = new Set(TEXT_FILE_EXTENSIONS); const FINGERPRINT_IGNORE_FILENAMES = Object.freeze([ '.gitignore', - '.clawhubignore', - '.clawdhubignore', + '.dt-skillignore', ]); const TEXT_SAMPLE_BYTES = 4096; diff --git a/dt-skill/package.json b/dt-skill/package.json index b92031ea..36a96d9a 100644 --- a/dt-skill/package.json +++ b/dt-skill/package.json @@ -1,16 +1,16 @@ { "name": "dt-skill", "version": "0.18.3", - "description": "ClawHub CLI \\u2014 install, update, search, and publish skills plus OpenClaw packages.", - "homepage": "https://clawhub.ai", + "description": "dt-skill CLI — install, update, search, and publish agent skills.", + "homepage": "https://github.com/TreeTreeDi/doraemon", "bugs": { - "url": "https://github.com/openclaw/clawhub/issues" + "url": "https://github.com/TreeTreeDi/doraemon/issues" }, "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/openclaw/clawhub.git", - "directory": "packages/clawhub" + "url": "git+https://github.com/TreeTreeDi/doraemon.git", + "directory": "dt-skill" }, "bin": { "dt-skill": "bin/dt-skill.js" diff --git a/dt-skill/src/cli/buildInfo.ts b/dt-skill/src/cli/buildInfo.ts index c3b09a2c..d06884be 100644 --- a/dt-skill/src/cli/buildInfo.ts +++ b/dt-skill/src/cli/buildInfo.ts @@ -24,8 +24,7 @@ function shortCommit(value: string) { function getCliCommit() { const candidates = [ - process.env.CLAWHUB_COMMIT, - process.env.CLAWDHUB_COMMIT, + process.env.DT_SKILL_COMMIT, process.env.VERCEL_GIT_COMMIT_SHA, process.env.GITHUB_SHA, process.env.COMMIT_SHA, diff --git a/dt-skill/src/cli/commands/github.test.ts b/dt-skill/src/cli/commands/github.test.ts index 076844aa..df222af6 100644 --- a/dt-skill/src/cli/commands/github.test.ts +++ b/dt-skill/src/cli/commands/github.test.ts @@ -9,7 +9,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { fetchGitHubSource, resolveLocalGitInfo, resolveSourceInput } from "./github"; async function makeTmpDir() { - return await mkdtemp(join(tmpdir(), "clawhub-github-test-")); + return await mkdtemp(join(tmpdir(), "dt-skill-github-test-")); } function runGit(cwd: string, args: string[]) { diff --git a/dt-skill/src/cli/commands/github.ts b/dt-skill/src/cli/commands/github.ts index a74561b8..13cbbe5c 100644 --- a/dt-skill/src/cli/commands/github.ts +++ b/dt-skill/src/cli/commands/github.ts @@ -6,7 +6,7 @@ import { unzipSync } from "fflate"; const GITHUB_API = "https://api.github.com"; const GITHUB_HOSTS = new Set(["github.com", "www.github.com"]); -const ZIP_USER_AGENT = "clawhub/package-publish"; +const ZIP_USER_AGENT = "dt-skill/package-publish"; type ResolvedPublishSource = | { @@ -90,7 +90,7 @@ export async function fetchGitHubSource( const archiveBytes = await downloadGitHubZip(source.owner, source.repo, commit, token); const entries = stripSingleTopLevelFolder(unzipSync(archiveBytes)); const publishPath = normalizeRepoSubpath(source.path); - const tempDir = await mkdtemp(join(tmpdir(), "clawhub-github-publish-")); + const tempDir = await mkdtemp(join(tmpdir(), "dt-skill-github-publish-")); try { const subdirEntries = filterEntriesForSubpath(entries, publishPath); diff --git a/dt-skill/src/cli/commands/skills.install.test.ts b/dt-skill/src/cli/commands/skills.install.test.ts index e06a28a5..a078b843 100644 --- a/dt-skill/src/cli/commands/skills.install.test.ts +++ b/dt-skill/src/cli/commands/skills.install.test.ts @@ -123,12 +123,12 @@ describe("cmdInstall with packages", () => { await cmdInstall(makeOpts(), "single-skill"); expect(mockApiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", expect.objectContaining({ path: "/api/v1/skills/single-skill" }), expect.anything() ); expect(mockDownloadZip).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", { slug: "single-skill", version: "1.0.0" } ); expect(extractZipToDirMock).toHaveBeenCalled(); @@ -167,12 +167,12 @@ describe("cmdInstall with packages", () => { await cmdInstall(makeOpts(), "no-version-skill"); expect(mockApiRequest).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", expect.objectContaining({ path: "/api/v1/skills/no-version-skill" }), expect.anything() ); expect(mockDownloadZip).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", { slug: "no-version-skill", version: "latest" } ); expect(extractZipToDirMock).toHaveBeenCalled(); @@ -271,7 +271,7 @@ describe("cmdInstall with packages", () => { expect(mockDownloadZip).toHaveBeenCalledTimes(1); expect(mockDownloadZip).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", { slug: "sub-2", version: "1.2.0" } ); diff --git a/dt-skill/src/cli/commands/skills.test.ts b/dt-skill/src/cli/commands/skills.test.ts index 534e8fb2..09befbc9 100644 --- a/dt-skill/src/cli/commands/skills.test.ts +++ b/dt-skill/src/cli/commands/skills.test.ts @@ -416,7 +416,7 @@ describe("cmdUpdate", () => { }); vi.mocked(readSkillOrigin).mockResolvedValue({ version: 1, - registry: "https://clawhub.ai", + registry: "https://example.com", slug: "demo", installedVersion: "1.0.0", installedAt: 123, @@ -438,12 +438,12 @@ describe("cmdUpdate", () => { "demo: local changes (no match). Use --force to overwrite.", ); expect(mockDownloadZip).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", expect.objectContaining({ slug: "demo", version: "2.0.0" }), ); expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/.demo-update-test", { version: 1, - registry: "https://clawhub.ai", + registry: "https://example.com", slug: "demo", installedVersion: "2.0.0", installedAt: 123, @@ -645,7 +645,7 @@ describe("cmdInstall", () => { expect(rm).not.toHaveBeenCalled(); expect(mockApiRequest).toHaveBeenNthCalledWith( 2, - "https://clawhub.ai", + "https://example.com", expect.objectContaining({ path: `${ApiRoutes.skills}/${encodeURIComponent("demo")}/versions/${encodeURIComponent("9.9.9")}`, }), @@ -692,7 +692,7 @@ describe("cmdInstall", () => { expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); expect(mockDownloadZip).toHaveBeenCalledWith( - "https://clawhub.ai", + "https://example.com", expect.objectContaining({ slug: "demo", version: "9.9.9" }), ); const versionLookupOrder = mockApiRequest.mock.invocationCallOrder[1]; diff --git a/dt-skill/src/cli/commands/skills.ts b/dt-skill/src/cli/commands/skills.ts index af3d71e1..de0a5bb4 100644 --- a/dt-skill/src/cli/commands/skills.ts +++ b/dt-skill/src/cli/commands/skills.ts @@ -199,7 +199,7 @@ export async function cmdInstall( const lock = await readLockfile(installWorkdir); const existingEntry = lock.skills[trimmed]; if (isPinnedSkillEntry(existingEntry)) { - fail(`skill "${trimmed}" is pinned; run \`clawhub unpin ${trimmed}\` first`); + fail(`skill "${trimmed}" is pinned; run \`dt-skill unpin ${trimmed}\` first`); } const spinner = createSpinner(`Resolving ${trimmed}`); @@ -400,7 +400,7 @@ export async function cmdUpdate( const lock = await readLockfile(installWorkdir); if (slug && isPinnedSkillEntry(lock.skills[slug])) { - fail(`skill "${slug}" is pinned; run \`clawhub unpin ${slug}\` first`); + fail(`skill "${slug}" is pinned; run \`dt-skill unpin ${slug}\` first`); } const allowPrompt = isInteractive() && inputAllowed; @@ -661,7 +661,7 @@ export async function cmdList(opts: GlobalOpts) { } if (manualSkills.length > 0) { if (entries.length > 0) console.log(); - console.log("Manually installed (not tracked by clawhub):"); + console.log("Manually installed (not tracked by dt-skill):"); for (const slug of manualSkills) { console.log(` ${slug}`); } diff --git a/dt-skill/src/cli/registry.test.ts b/dt-skill/src/cli/registry.test.ts index 3760767d..a0562feb 100644 --- a/dt-skill/src/cli/registry.test.ts +++ b/dt-skill/src/cli/registry.test.ts @@ -42,8 +42,8 @@ describe("registry resolution", () => { }); it("prefers explicit registry over discovery/cache", async () => { - readGlobalConfig.mockResolvedValue({ registry: "https://auth.clawdhub.com" }); - discoverRegistryFromSite.mockResolvedValue({ apiBase: "https://clawhub.ai" }); + readGlobalConfig.mockResolvedValue({ registry: "https://cached.example" }); + discoverRegistryFromSite.mockResolvedValue({ apiBase: "https://discovered.example" }); const registry = await resolveRegistry( makeOpts({ registry: "https://custom.example", registrySource: "cli" }), @@ -53,8 +53,18 @@ describe("registry resolution", () => { expect(discoverRegistryFromSite).not.toHaveBeenCalled(); }); - it("ignores legacy registry and updates cache from discovery", async () => { - readGlobalConfig.mockResolvedValue({ registry: "https://auth.clawdhub.com" }); + it("uses cached registry before site discovery", async () => { + readGlobalConfig.mockResolvedValue({ registry: "http://10.0.0.7:7001" }); + discoverRegistryFromSite.mockResolvedValue({ apiBase: "http://10.0.0.8:7001" }); + + const registry = await resolveRegistry(makeOpts({ site: "http://10.0.0.8:7001" })); + + expect(registry).toBe("http://10.0.0.7:7001"); + expect(discoverRegistryFromSite).not.toHaveBeenCalled(); + }); + + it("discovers registry from site when cache is empty", async () => { + readGlobalConfig.mockResolvedValue(null); discoverRegistryFromSite.mockResolvedValue({ apiBase: "http://10.0.0.8:7001" }); const registry = await getRegistry(makeOpts({ site: "http://10.0.0.8:7001" }), { cache: true }); @@ -66,7 +76,7 @@ describe("registry resolution", () => { }); it("fails clearly when no explicit, cached, or discoverable registry exists", async () => { - readGlobalConfig.mockResolvedValue({ registry: "https://registry.clawhub.ai" }); + readGlobalConfig.mockResolvedValue(null); discoverRegistryFromSite.mockResolvedValue(null); await expect(getRegistry(makeOpts(), { cache: true })).rejects.toThrow( diff --git a/dt-skill/src/cli/registry.ts b/dt-skill/src/cli/registry.ts index 31f6e289..9391888a 100644 --- a/dt-skill/src/cli/registry.ts +++ b/dt-skill/src/cli/registry.ts @@ -4,12 +4,6 @@ import type { GlobalOpts } from "./types.js"; export const DEFAULT_SITE = ""; export const DEFAULT_REGISTRY = ""; -const LEGACY_REGISTRY_HOSTS = new Set([ - "auth.clawdhub.com", - "auth.clawhub.com", - "auth.clawhub.ai", - "registry.clawhub.ai", -]); export async function resolveRegistry(opts: GlobalOpts) { const explicit = opts.registrySource !== "default" ? opts.registry.trim() : ""; @@ -17,7 +11,7 @@ export async function resolveRegistry(opts: GlobalOpts) { const cfg = await readGlobalConfig(); const cached = cfg?.registry?.trim(); - if (cached && !isLegacyRegistry(cached)) return cached; + if (cached) return cached; const site = opts.site.trim(); if (site) { @@ -37,18 +31,6 @@ export async function getRegistry(opts: GlobalOpts, params?: { cache?: boolean } if (!cache) return registry; const cfg = await readGlobalConfig(); const cached = cfg?.registry?.trim(); - const shouldUpdate = - !cached || - isLegacyRegistry(cached) || - cached !== registry; - if (shouldUpdate) await writeGlobalConfig({ registry }); + if (!cached || cached !== registry) await writeGlobalConfig({ registry }); return registry; } - -function isLegacyRegistry(registry: string) { - try { - return LEGACY_REGISTRY_HOSTS.has(new URL(registry).hostname); - } catch { - return false; - } -} diff --git a/dt-skill/src/cli/ui.test.ts b/dt-skill/src/cli/ui.test.ts index 6676c64f..12a23d15 100644 --- a/dt-skill/src/cli/ui.test.ts +++ b/dt-skill/src/cli/ui.test.ts @@ -31,7 +31,7 @@ describe("openInBrowser", () => { const child = createMockChild(); mockSpawn.mockReturnValueOnce(child); const url = - "https://clawhub.ai/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2Fcallback&state=abc123"; + "https://example.com/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2Fcallback&state=abc123"; try { Object.defineProperty(process, "platform", { value: "win32" }); @@ -52,12 +52,12 @@ describe("openInBrowser", () => { mockSpawn.mockReturnValueOnce(child); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - openInBrowser("https://clawhub.ai"); + openInBrowser("https://example.com"); child.emitError(Object.assign(new Error("not found"), { code: "ENOENT" })); expect(logSpy).toHaveBeenCalledWith("Could not open browser automatically."); expect(logSpy).toHaveBeenCalledWith("Please open this URL manually:"); - expect(logSpy).toHaveBeenCalledWith(" https://clawhub.ai"); + expect(logSpy).toHaveBeenCalledWith(" https://example.com"); expect(child.unref).toHaveBeenCalledOnce(); logSpy.mockRestore(); }); @@ -67,7 +67,7 @@ describe("openInBrowser", () => { mockSpawn.mockReturnValueOnce(child); const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - openInBrowser("https://clawhub.ai"); + openInBrowser("https://example.com"); child.emitError(Object.assign(new Error("permission denied"), { code: "EACCES" })); expect(logSpy).not.toHaveBeenCalledWith("Could not open browser automatically."); diff --git a/dt-skill/src/config.test.ts b/dt-skill/src/config.test.ts index fed456dc..a378538a 100644 --- a/dt-skill/src/config.test.ts +++ b/dt-skill/src/config.test.ts @@ -26,7 +26,7 @@ const configModuleSpecifier = "./config.js?config-test" as string; const { writeGlobalConfig } = (await import(configModuleSpecifier)) as typeof import("./config"); const originalPlatform = process.platform; -const testConfigPath = "/tmp/clawhub-config-test/config.json"; +const testConfigPath = "/tmp/dt-skill-config-test/config.json"; const envStubs = createEnvStubRegistry(); function makeErr(code: string): NodeJS.ErrnoException { @@ -36,7 +36,7 @@ function makeErr(code: string): NodeJS.ErrnoException { } beforeEach(() => { - envStubs.stub("CLAWHUB_CONFIG_PATH", testConfigPath); + envStubs.stub("DT_SKILL_CONFIG_PATH", testConfigPath); Object.defineProperty(process, "platform", { value: "linux" }); fsMocks.chmod.mockResolvedValue(undefined); fsMocks.mkdir.mockResolvedValue(undefined); @@ -58,7 +58,7 @@ describe("writeGlobalConfig", () => { it("writes config with restricted modes", async () => { await writeGlobalConfig({ registry: "https://example.com" }); - expect(fsMocks.mkdir).toHaveBeenCalledWith("/tmp/clawhub-config-test", { + expect(fsMocks.mkdir).toHaveBeenCalledWith("/tmp/dt-skill-config-test", { recursive: true, mode: 0o700, }); diff --git a/dt-skill/src/config.ts b/dt-skill/src/config.ts index e114111a..70205269 100644 --- a/dt-skill/src/config.ts +++ b/dt-skill/src/config.ts @@ -1,19 +1,10 @@ -import { existsSync } from "node:fs"; import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; import { resolveHome } from "./homedir.js"; import { type GlobalConfig, GlobalConfigSchema, parseArk } from "./schema/index.js"; -/** - * Resolve config path with legacy fallback. - * Checks for 'clawhub' first, falls back to legacy 'clawdhub' if it exists. - */ function resolveConfigPath(baseDir: string): string { - const clawhubPath = join(baseDir, "clawhub", "config.json"); - const clawdhubPath = join(baseDir, "clawdhub", "config.json"); - if (existsSync(clawhubPath)) return clawhubPath; - if (existsSync(clawdhubPath)) return clawdhubPath; - return clawhubPath; + return join(baseDir, "dt-skill", "config.json"); } function isNonFatalChmodError(error: unknown): boolean { @@ -23,8 +14,7 @@ function isNonFatalChmodError(error: unknown): boolean { } function getGlobalConfigPath() { - const override = - process.env.CLAWHUB_CONFIG_PATH?.trim() ?? process.env.CLAWDHUB_CONFIG_PATH?.trim(); + const override = process.env.DT_SKILL_CONFIG_PATH?.trim(); if (override) return resolve(override); const home = resolveHome(); diff --git a/dt-skill/src/discovery.ts b/dt-skill/src/discovery.ts index b7807ac3..dccf7bb9 100644 --- a/dt-skill/src/discovery.ts +++ b/dt-skill/src/discovery.ts @@ -1,22 +1,18 @@ import { parseArk, WellKnownConfigSchema } from "./schema/index.js"; export async function discoverRegistryFromSite(siteUrl: string) { - const paths = ["/.well-known/clawhub.json", "/.well-known/clawdhub.json"]; - for (const path of paths) { - const url = new URL(path, siteUrl); - const response = await fetch(url.toString(), { - method: "GET", - headers: { Accept: "application/json" }, - }); - if (!response.ok) continue; - const raw = (await response.json()) as unknown; - const parsed = parseArk(WellKnownConfigSchema, raw, "WellKnown config"); - const apiBase = "apiBase" in parsed ? parsed.apiBase : parsed.registry; - if (!apiBase) return null; - return { - apiBase, - minCliVersion: parsed.minCliVersion, - }; - } - return null; + const url = new URL("/.well-known/dt-skill.json", siteUrl); + const response = await fetch(url.toString(), { + method: "GET", + headers: { Accept: "application/json" }, + }); + if (!response.ok) return null; + const raw = (await response.json()) as unknown; + const parsed = parseArk(WellKnownConfigSchema, raw, "WellKnown config"); + const apiBase = "apiBase" in parsed ? parsed.apiBase : parsed.registry; + if (!apiBase) return null; + return { + apiBase, + minCliVersion: parsed.minCliVersion, + }; } diff --git a/dt-skill/src/http.bun.test.ts b/dt-skill/src/http.bun.test.ts index dbe00158..a3f89229 100644 --- a/dt-skill/src/http.bun.test.ts +++ b/dt-skill/src/http.bun.test.ts @@ -16,7 +16,7 @@ function createBunClient(options?: { }) { const spawnImpl = vi.fn(options?.spawnImpl ?? (() => ({ status: 0, stdout: "", stderr: "" }))); const mkdirImpl = vi.fn(async () => undefined); - const mkdtempImpl = vi.fn(async () => options?.mkdtempValue ?? "/tmp/clawhub-test"); + const mkdtempImpl = vi.fn(async () => options?.mkdtempValue ?? "/tmp/dt-skill-test"); const rmImpl = vi.fn(async () => undefined); const writeFileImpl = vi.fn(async () => undefined); const readFileImpl = vi.fn( @@ -111,7 +111,7 @@ describe("bun http client", () => { const { client, spawnImpl } = createBunClient({ spawnImpl: () => ({ status: 0, - stdout: "rate limited\n__CLAWHUB_CURL_META__\n429\n20\n0\n1771404540\n20\n0\n34\n34\n", + stdout: "rate limited\n__DT_SKILL_CURL_META__\n429\n20\n0\n1771404540\n20\n0\n34\n34\n", stderr: "", }), }); @@ -132,7 +132,7 @@ describe("bun http client", () => { .mockReturnValueOnce({ status: 0, stdout: "hello world\n200", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "200", stderr: "" }) .mockReturnValueOnce({ status: 0, stdout: "404", stderr: "" }), - mkdtempValue: "/tmp/clawhub-download-abc", + mkdtempValue: "/tmp/dt-skill-download-abc", readFileValue: Buffer.from("not found"), }); @@ -148,7 +148,7 @@ describe("bun http client", () => { ).rejects.toThrow("not found"); expect(readFileImpl).toHaveBeenCalled(); - expect(rmImpl).toHaveBeenCalledWith("/tmp/clawhub-download-abc", { + expect(rmImpl).toHaveBeenCalledWith("/tmp/dt-skill-download-abc", { recursive: true, force: true, }); @@ -158,7 +158,7 @@ describe("bun http client", () => { it("posts multipart form data via curl and cleans up temp files", async () => { const { client, spawnImpl, mkdirImpl, writeFileImpl, rmImpl } = createBunClient({ spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: "" }), - mkdtempValue: "/tmp/clawhub-upload-abc", + mkdtempValue: "/tmp/dt-skill-upload-abc", }); const form = new FormData(); @@ -172,16 +172,16 @@ describe("bun http client", () => { }); expect(result).toEqual({ ok: true }); - expect(mkdirImpl).toHaveBeenCalledWith("/tmp/clawhub-upload-abc/dist", { recursive: true }); + expect(mkdirImpl).toHaveBeenCalledWith("/tmp/dt-skill-upload-abc/dist", { recursive: true }); expect(writeFileImpl).toHaveBeenCalled(); - expect(rmImpl).toHaveBeenCalledWith("/tmp/clawhub-upload-abc", { + expect(rmImpl).toHaveBeenCalledWith("/tmp/dt-skill-upload-abc", { recursive: true, force: true, }); const [, args] = spawnImpl.mock.calls[0] as [string, string[]]; expect(args).toContain("-F"); expect(args.some((arg) => arg.includes("name=demo"))).toBe(true); - expect(args.some((arg) => arg.includes("file=@/tmp/clawhub-upload-abc/dist/demo.txt"))).toBe( + expect(args.some((arg) => arg.includes("file=@/tmp/dt-skill-upload-abc/dist/demo.txt"))).toBe( true, ); }); diff --git a/dt-skill/src/http.test.ts b/dt-skill/src/http.test.ts index 1446ea04..6ccba019 100644 --- a/dt-skill/src/http.test.ts +++ b/dt-skill/src/http.test.ts @@ -88,8 +88,8 @@ describe("shouldUseProxyFromEnv", () => { describe("registryUrl", () => { it("preserves registry base paths and normalizes slashes", () => { - expect(registryUrl("/api/v1/skills", "https://clawhub.ai").toString()).toBe( - "https://clawhub.ai/api/v1/skills", + expect(registryUrl("/api/v1/skills", "https://example.com").toString()).toBe( + "https://example.com/api/v1/skills", ); expect(registryUrl("/api/v1/skills", "http://localhost:8081/custom/path").toString()).toBe( "http://localhost:8081/custom/path/api/v1/skills", diff --git a/dt-skill/src/http.ts b/dt-skill/src/http.ts index 9820f10e..8780eb0b 100644 --- a/dt-skill/src/http.ts +++ b/dt-skill/src/http.ts @@ -15,7 +15,7 @@ const RETRY_COUNT = 2; const RETRY_BACKOFF_BASE_MS = 300; const RETRY_BACKOFF_MAX_MS = 5_000; const RETRY_AFTER_JITTER_MS = 250; -const CURL_META_MARKER = "__CLAWHUB_CURL_META__"; +const CURL_META_MARKER = "__DT_SKILL_CURL_META__"; const CURL_WRITE_OUT_FORMAT = [ "", CURL_META_MARKER, @@ -626,7 +626,7 @@ async function fetchJsonFormViaCurl( ) { const headers = ["-H", "Accept: application/json"]; - const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "clawhub-upload-")); + const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "dt-skill-upload-")); try { const formArgs: string[] = []; for (const [key, value] of args.form.entries()) { @@ -707,7 +707,7 @@ async function fetchBinaryViaCurl( >, url: string, ) { - const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "clawhub-download-")); + const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "dt-skill-download-")); const filePath = join(tempDir, "payload.bin"); try { const headers: string[] = []; diff --git a/dt-skill/src/schema/schemas.test.ts b/dt-skill/src/schema/schemas.test.ts index 67f4af63..468d680b 100644 --- a/dt-skill/src/schema/schemas.test.ts +++ b/dt-skill/src/schema/schemas.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { parseArk } from "./ark"; import { ApiV1SearchResponseSchema, ClawdisSkillMetadataSchema } from "./schemas"; -describe("packages/clawhub skill metadata schema", () => { +describe("dt-skill skill metadata schema", () => { it("preserves optional env var declarations", () => { const parsed = parseArk( ClawdisSkillMetadataSchema, diff --git a/dt-skill/src/schema/textFiles.test.ts b/dt-skill/src/schema/textFiles.test.ts index 26309f99..e6bb1f6b 100644 --- a/dt-skill/src/schema/textFiles.test.ts +++ b/dt-skill/src/schema/textFiles.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import * as schema from "."; import { isTextContentType, TEXT_FILE_EXTENSION_SET } from "./textFiles"; -describe("packages/clawhub schema textFiles", () => { +describe("dt-skill schema textFiles", () => { it("exports text-file extension set", () => { expect(TEXT_FILE_EXTENSION_SET.has("md")).toBe(true); expect(TEXT_FILE_EXTENSION_SET.has("r")).toBe(true); diff --git a/dt-skill/src/skills.test.ts b/dt-skill/src/skills.test.ts index 9e59ec85..cdae8534 100644 --- a/dt-skill/src/skills.test.ts +++ b/dt-skill/src/skills.test.ts @@ -21,7 +21,7 @@ import { describe("skills", () => { it("extracts zip into directory and skips traversal", async () => { - const parent = await mkdtemp(join(tmpdir(), "clawhub-zip-")); + const parent = await mkdtemp(join(tmpdir(), "dt-skill-zip-")); const dir = join(parent, "dir"); await mkdir(dir); const evilName = `evil-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; @@ -36,7 +36,7 @@ describe("skills", () => { }); it("writes and reads lockfile", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-work-")); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-work-")); await writeLockfile(workdir, { version: 1, skills: { @@ -55,18 +55,18 @@ describe("skills", () => { }); it("returns empty lockfile on invalid json", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-work-bad-")); - await mkdir(join(workdir, ".clawhub"), { recursive: true }); - await writeFile(join(workdir, ".clawhub", "lock.json"), "{", "utf8"); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-work-bad-")); + await mkdir(join(workdir, ".dt-skill"), { recursive: true }); + await writeFile(join(workdir, ".dt-skill", "lock.json"), "{", "utf8"); const read = await readLockfile(workdir); expect(read).toEqual({ version: 1, skills: {} }); }); it("returns empty lockfile on schema mismatch", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-work-schema-")); - await mkdir(join(workdir, ".clawhub"), { recursive: true }); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-work-schema-")); + await mkdir(join(workdir, ".dt-skill"), { recursive: true }); await writeFile( - join(workdir, ".clawhub", "lock.json"), + join(workdir, ".dt-skill", "lock.json"), JSON.stringify({ version: 1, skills: "nope" }), "utf8", ); @@ -75,21 +75,21 @@ describe("skills", () => { }); it("skips dotfiles and node_modules when listing text files", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-files-")); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-files-")); await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); await writeFile(join(workdir, ".secret.txt"), "no", "utf8"); - await mkdir(join(workdir, ".clawhub"), { recursive: true }); - await writeFile(join(workdir, ".clawhub", "origin.json"), "{}", "utf8"); + await mkdir(join(workdir, ".dt-skill"), { recursive: true }); + await writeFile(join(workdir, ".dt-skill", "origin.json"), "{}", "utf8"); await mkdir(join(workdir, "node_modules"), { recursive: true }); await writeFile(join(workdir, "node_modules", "a.txt"), "no", "utf8"); const files = await listTextFiles(workdir); expect(files.map((file) => file.relPath)).toEqual(["SKILL.md"]); }); - it("respects .gitignore and .clawhubignore", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-ignore-")); + it("respects .gitignore and .dt-skillignore", async () => { + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-ignore-")); await writeFile(join(workdir, ".gitignore"), "ignored.md\n", "utf8"); - await writeFile(join(workdir, ".clawhubignore"), "private.md\n", "utf8"); + await writeFile(join(workdir, ".dt-skillignore"), "private.md\n", "utf8"); await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); await writeFile(join(workdir, "ignored.md"), "no", "utf8"); await writeFile(join(workdir, "private.md"), "no", "utf8"); @@ -105,7 +105,7 @@ describe("skills", () => { }); it("falls back to text/plain for unknown text extensions", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-env-")); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-env-")); await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); await writeFile(join(workdir, "config.env"), "TOKEN=demo", "utf8"); const files = await listTextFiles(workdir); @@ -113,7 +113,7 @@ describe("skills", () => { }); it("includes tsv and extensionless text files while skipping extensionless binaries", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-extensionless-")); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-extensionless-")); await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); await writeFile(join(workdir, "config.tsv"), "name\tvalue\napi\tok\n", "utf8"); await writeFile(join(workdir, ".npmrc"), "//registry.npmjs.org/:_authToken=secret\n", "utf8"); @@ -194,26 +194,26 @@ describe("skills", () => { }); it("returns null for invalid skill origin metadata", async () => { - const workdir = await mkdtemp(join(tmpdir(), "clawhub-origin-")); + const workdir = await mkdtemp(join(tmpdir(), "dt-skill-origin-")); expect(await readSkillOrigin(workdir)).toBeNull(); - await mkdir(join(workdir, ".clawhub"), { recursive: true }); + await mkdir(join(workdir, ".dt-skill"), { recursive: true }); await writeFile( - join(workdir, ".clawhub", "origin.json"), + join(workdir, ".dt-skill", "origin.json"), JSON.stringify({ version: 2 }), "utf8", ); expect(await readSkillOrigin(workdir)).toBeNull(); await writeFile( - join(workdir, ".clawhub", "origin.json"), + join(workdir, ".dt-skill", "origin.json"), JSON.stringify({ version: 1, registry: "demo", slug: "x", installedAt: 1 }), "utf8", ); expect(await readSkillOrigin(workdir)).toBeNull(); await writeFile( - join(workdir, ".clawhub", "origin.json"), + join(workdir, ".dt-skill", "origin.json"), JSON.stringify({ version: 1, registry: "demo", @@ -238,7 +238,7 @@ describe("skills", () => { describe("listManualSkills", () => { it("lists manual skills not present in the lockfile", async () => { - const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-")); + const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-")); await mkdir(join(dir, "manual-skill")); await writeFile(join(dir, "manual-skill", "SKILL.md"), "# Manual", "utf8"); @@ -249,19 +249,17 @@ describe("skills", () => { expect(result).toEqual(["manual-skill"]); }); - it("recognizes skills from current and legacy origin metadata", async () => { - const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-origin-")); - await mkdir(join(dir, "current", ".clawhub"), { recursive: true }); - await writeFile(join(dir, "current", ".clawhub", "origin.json"), "{}", "utf8"); - await mkdir(join(dir, "legacy", ".clawdhub"), { recursive: true }); - await writeFile(join(dir, "legacy", ".clawdhub", "origin.json"), "{}", "utf8"); + it("recognizes skills from origin metadata", async () => { + const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-origin-")); + await mkdir(join(dir, "with-origin", ".dt-skill"), { recursive: true }); + await writeFile(join(dir, "with-origin", ".dt-skill", "origin.json"), "{}", "utf8"); const result = await listManualSkills(dir, new Set()); - expect(result).toEqual(["current", "legacy"]); + expect(result).toEqual(["with-origin"]); }); it("skips hidden and non-skill directories and returns sorted results", async () => { - const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-sort-")); + const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-sort-")); await mkdir(join(dir, "z-skill")); await writeFile(join(dir, "z-skill", "SKILL.md"), "# Z", "utf8"); await mkdir(join(dir, "a-skill")); @@ -276,7 +274,7 @@ describe("skills", () => { }); it("returns an empty list when the skills directory does not exist", async () => { - const dir = await mkdtemp(join(tmpdir(), "clawhub-manual-missing-")); + const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-missing-")); const result = await listManualSkills(join(dir, "missing"), new Set()); expect(result).toEqual([]); }); diff --git a/dt-skill/src/skills.ts b/dt-skill/src/skills.ts index bd225dd5..e4ea7b63 100644 --- a/dt-skill/src/skills.ts +++ b/dt-skill/src/skills.ts @@ -19,10 +19,8 @@ import { TEXT_SAMPLE_BYTES, } from "./schema/skillFingerprintContract.js"; -const DOT_DIR = ".clawhub"; -const LEGACY_DOT_DIR = ".clawdhub"; -const DOT_IGNORE = ".clawhubignore"; -const LEGACY_DOT_IGNORE = ".clawdhubignore"; +const DOT_DIR = ".dt-skill"; +const DOT_IGNORE = ".dt-skillignore"; export type SkillOrigin = { version: 1; @@ -115,17 +113,14 @@ export function hashSkillZip(zipBytes: Uint8Array) { } export async function readLockfile(workdir: string): Promise { - const paths = [join(workdir, DOT_DIR, "lock.json"), join(workdir, LEGACY_DOT_DIR, "lock.json")]; - for (const path of paths) { - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON.parse(raw) as unknown; - return parseArk(LockfileSchema, parsed, "Lockfile"); - } catch { - // try next - } + const path = join(workdir, DOT_DIR, "lock.json"); + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as unknown; + return parseArk(LockfileSchema, parsed, "Lockfile"); + } catch { + return { version: 1, skills: {} }; } - return { version: 1, skills: {} }; } export async function writeLockfile(workdir: string, lock: Lockfile) { @@ -135,32 +130,26 @@ export async function writeLockfile(workdir: string, lock: Lockfile) { } export async function readSkillOrigin(skillFolder: string): Promise { - const paths = [ - join(skillFolder, DOT_DIR, "origin.json"), - join(skillFolder, LEGACY_DOT_DIR, "origin.json"), - ]; - for (const path of paths) { - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON.parse(raw) as Partial; - if (parsed.version !== 1) return null; - if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null; - if (typeof parsed.installedAt !== "number" || !Number.isFinite(parsed.installedAt)) { - return null; - } - return { - version: 1, - registry: parsed.registry, - slug: parsed.slug, - installedVersion: parsed.installedVersion, - installedAt: parsed.installedAt, - fingerprint: typeof parsed.fingerprint === "string" ? parsed.fingerprint : undefined, - }; - } catch { - // try next + const path = join(skillFolder, DOT_DIR, "origin.json"); + try { + const raw = await readFile(path, "utf8"); + const parsed = JSON.parse(raw) as Partial; + if (parsed.version !== 1) return null; + if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null; + if (typeof parsed.installedAt !== "number" || !Number.isFinite(parsed.installedAt)) { + return null; } + return { + version: 1, + registry: parsed.registry, + slug: parsed.slug, + installedVersion: parsed.installedVersion, + installedAt: parsed.installedAt, + fingerprint: typeof parsed.fingerprint === "string" ? parsed.fingerprint : undefined, + }; + } catch { + return null; } - return null; } export async function writeSkillOrigin(skillFolder: string, origin: SkillOrigin) { @@ -225,10 +214,9 @@ async function addIgnoreFile(ig: ReturnType, path: string) { async function createSkillIgnoreMatcher(root: string) { const absRoot = resolve(root); const ig = ignore(); - ig.add([".git/", "node_modules/", `${DOT_DIR}/`, `${LEGACY_DOT_DIR}/`]); + ig.add([".git/", "node_modules/", `${DOT_DIR}/`]); await addIgnoreFile(ig, join(absRoot, ".gitignore")); await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); - await addIgnoreFile(ig, join(absRoot, LEGACY_DOT_IGNORE)); return { absRoot, ig }; } @@ -257,7 +245,6 @@ async function hasSkillMetadata(skillDir: string) { const candidates = [ join(skillDir, "SKILL.md"), join(skillDir, DOT_DIR, "origin.json"), - join(skillDir, LEGACY_DOT_DIR, "origin.json"), ]; for (const path of candidates) { try { diff --git a/dt-skill/test/cliCommandTestKit.ts b/dt-skill/test/cliCommandTestKit.ts index eb864d95..aee73f3e 100644 --- a/dt-skill/test/cliCommandTestKit.ts +++ b/dt-skill/test/cliCommandTestKit.ts @@ -6,8 +6,8 @@ export function makeGlobalOpts(workdir = "/work"): GlobalOpts { return { workdir, dir: join(workdir, "skills"), - site: "https://clawhub.ai", - registry: "https://clawhub.ai", + site: "https://example.com", + registry: "https://example.com", registrySource: "default", }; } @@ -47,7 +47,7 @@ export function createHttpModuleMocks() { } export function createRegistryModuleMocks() { - const getRegistry = vi.fn(async (_opts?: unknown, _params?: unknown) => "https://clawhub.ai"); + const getRegistry = vi.fn(async (_opts?: unknown, _params?: unknown) => "https://example.com"); return { getRegistry, diff --git a/test/clawhub-contract.test.js b/test/skills-registry-contract.test.js similarity index 91% rename from test/clawhub-contract.test.js rename to test/skills-registry-contract.test.js index 0b0aa9ee..5afbbe41 100644 --- a/test/clawhub-contract.test.js +++ b/test/skills-registry-contract.test.js @@ -43,14 +43,14 @@ function createMockCtx(query = {}, params = {}, body = {}, files = []) { } // Load the service module -const ClawhubService = require('../app/service/clawhub'); +const SkillsRegistryService = require('../app/service/skillsRegistry'); // ============================================================ // Phase 2: Foundation Tests (T005) // ============================================================ -test('ClawhubService can be instantiated with mock app and ctx', () => { - const service = Object.create(ClawhubService.prototype); +test('SkillsRegistryService can be instantiated with mock app and ctx', () => { + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp(); service.ctx = createMockCtx(); assert.equal(typeof service.getRegistryMetadata, 'function'); @@ -61,7 +61,7 @@ test('ClawhubService can be instantiated with mock app and ctx', () => { // ============================================================ test('getRegistryMetadata returns well-known config', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp(); service.ctx = createMockCtx(); @@ -72,7 +72,7 @@ test('getRegistryMetadata returns well-known config', async () => { }); test('searchSkills returns flat results with score 1.0', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findAll: async () => [ @@ -102,7 +102,7 @@ test('searchSkills returns flat results with score 1.0', async () => { }); test('searchSkills with empty query returns all skills', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findAll: async () => [], @@ -115,7 +115,7 @@ test('searchSkills with empty query returns all skills', async () => { }); test('listSkills returns items with cursor pagination', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findAll: async () => [ @@ -189,7 +189,7 @@ test('listSkills uses a composite cursor that matches newest sorting', async () updated_at: new Date('2026-05-21T10:00:00Z'), }, ]; - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findAll: async (options) => { @@ -238,7 +238,7 @@ test('listSkills uses a composite cursor that matches stars sorting', async () = updated_at: new Date('2026-05-21T09:00:00Z'), }, ]; - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findAll: async (options) => { @@ -266,7 +266,7 @@ test('listSkills uses a composite cursor that matches stars sorting', async () = }); test('getSkillDetail returns full skill object', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ @@ -304,7 +304,7 @@ test('getSkillDetail returns full skill object', async () => { }); test('getSkillDetail returns null for missing skill', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => null, @@ -317,7 +317,7 @@ test('getSkillDetail returns null for missing skill', async () => { }); test('getSkillDetail returns children for a package skill', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ @@ -364,7 +364,7 @@ test('getSkillDetail returns children for a package skill', async () => { }); test('listSkillVersions returns single version list', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ @@ -387,7 +387,7 @@ test('listSkillVersions returns single version list', async () => { }); test('getSkillVersionDetail returns version for matching current version', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ @@ -412,7 +412,7 @@ test('getSkillVersionDetail returns version for matching current version', async }); test('getSkillVersionDetail returns null for non-matching version', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ @@ -434,7 +434,7 @@ test('getSkillVersionDetail returns null for non-matching version', async () => // ============================================================ test('getSkillFileContent returns file content', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ id: 1, slug: 'my-skill' }), @@ -456,7 +456,7 @@ test('getSkillFileContent returns file content', async () => { }); test('getSkillFileContent returns null for missing file', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ id: 1, slug: 'my-skill' }), @@ -472,7 +472,7 @@ test('getSkillFileContent returns null for missing file', async () => { }); test('buildSkillZip returns zip buffer', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ id: 1, slug: 'my-skill', version: '1.0.0' }), @@ -491,7 +491,7 @@ test('buildSkillZip returns zip buffer', async () => { }); test('buildSkillZip packages skill package nested structure when is_package is 1', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async (options) => { @@ -546,7 +546,7 @@ test('buildSkillZip packages skill package nested structure when is_package is 1 // ============================================================ test('validateSemVer accepts valid versions', () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); assert.equal(service.validateSemVer('1.0.0'), true); assert.equal(service.validateSemVer('0.1.0'), true); assert.equal(service.validateSemVer('1.2.3-alpha'), true); @@ -554,7 +554,7 @@ test('validateSemVer accepts valid versions', () => { }); test('validateSemVer rejects invalid versions', () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); assert.equal(service.validateSemVer('v1.0.0'), false); assert.equal(service.validateSemVer('1.0'), false); assert.equal(service.validateSemVer('1.0.0.0'), false); @@ -562,7 +562,7 @@ test('validateSemVer rejects invalid versions', () => { }); test('isBinaryBuffer detects invalid UTF-8 content', () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); assert.equal(service.isBinaryBuffer(Buffer.from('valid utf8', 'utf8')), false); assert.equal(service.isBinaryBuffer(Buffer.from([0xff, 0xfe, 0xfd])), true); @@ -570,7 +570,7 @@ test('isBinaryBuffer detects invalid UTF-8 content', () => { }); test('publishSkill rejects missing SKILL.md', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => null }, SkillsSource: { findOne: async () => null, create: async () => ({ id: 1 }) }, @@ -592,7 +592,7 @@ test('publishSkill rejects missing SKILL.md', async () => { }); test('publishSkill returns ok: true and string skillId and versionId', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => null, @@ -624,7 +624,7 @@ test('publishSkill returns ok: true and string skillId and versionId', async () // ============================================================ test('computeSkillFingerprint returns consistent hex string', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsFile: { findAll: async () => [ @@ -644,7 +644,7 @@ test('computeSkillFingerprint returns consistent hex string', async () => { test('computeSkillFingerprint matches shared golden vectors', async () => { const testCase = goldenVectors.cases.find((entry) => entry.name === 'basic-markdown-pair'); - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsFile: { findAll: async () => [ @@ -669,7 +669,7 @@ test('computeSkillFingerprint matches shared golden vectors', async () => { test('computeSkillFingerprint uses the same text-file set as the CLI', async () => { const testCase = goldenVectors.cases.find((entry) => entry.name === 'text-file-set-filtering'); - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsFile: { findAll: async () => [ @@ -694,7 +694,7 @@ test('computeSkillFingerprint uses the same text-file set as the CLI', async () test('computeSkillFingerprint applies stored ignore files like the CLI', async () => { const testCase = goldenVectors.cases.find((entry) => entry.name === 'stored-ignore-files'); - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsFile: { findAll: async () => [ @@ -718,7 +718,7 @@ test('computeSkillFingerprint applies stored ignore files like the CLI', async ( }); test('resolveFingerprint returns match and latestVersion for matching fingerprint', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ id: 1, slug: 'skill-a', version: '1.0.0' }), @@ -737,7 +737,7 @@ test('resolveFingerprint returns match and latestVersion for matching fingerprin }); test('resolveFingerprint returns null match for unmatched fingerprint but valid slug', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => ({ id: 1, slug: 'skill-a', version: '1.0.0' }), @@ -755,7 +755,7 @@ test('resolveFingerprint returns null match for unmatched fingerprint but valid }); test('resolveFingerprint returns null values for missing slug', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); service.app = createMockApp({ SkillsItem: { findOne: async () => null, @@ -774,7 +774,7 @@ test('resolveFingerprint returns null values for missing slug', async () => { // ============================================================ test('deleteSkill sets is_delete to 1', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); const skill = { update: async (data) => Object.assign(skill, data) }; service.app = createMockApp({ SkillsItem: { @@ -789,7 +789,7 @@ test('deleteSkill sets is_delete to 1', async () => { }); test('undeleteSkill sets is_delete to 0', async () => { - const service = Object.create(ClawhubService.prototype); + const service = Object.create(SkillsRegistryService.prototype); const skill = { is_delete: 1, update: async (data) => Object.assign(skill, data) }; service.app = createMockApp({ SkillsItem: { @@ -807,12 +807,12 @@ test('undeleteSkill sets is_delete to 0', async () => { // Controller Tests // ============================================================ -test('ClawhubController returns flat JSON (no wrapper)', async () => { - const ClawhubController = require('../app/controller/clawhub'); - const controller = Object.create(ClawhubController.prototype); +test('SkillsRegistryController returns flat JSON (no wrapper)', async () => { + const SkillsRegistryController = require('../app/controller/skillsRegistry'); + const controller = Object.create(SkillsRegistryController.prototype); controller.ctx = createMockCtx(); controller.ctx.service = { - clawhub: { + skillsRegistry: { getRegistryMetadata: async () => ({ apiBase: '/api/v1' }), }, }; diff --git a/test/clawhub-integration.test.js b/test/skills-registry-integration.test.js similarity index 94% rename from test/clawhub-integration.test.js rename to test/skills-registry-integration.test.js index d6a8a491..06410753 100644 --- a/test/clawhub-integration.test.js +++ b/test/skills-registry-integration.test.js @@ -2,15 +2,15 @@ const test = require('node:test'); const assert = require('node:assert/strict'); /** - * Integration tests for clawhub API endpoints. + * Integration tests for skills registry API endpoints. * * These tests verify the controller + service integration with mocked * Egg.js context. Full end-to-end tests requiring a running dev server - * and the clawhub CLI should be run manually per quickstart.md. + * and the dt-skill CLI should be run manually per quickstart.md. */ // Load controller and create mock ctx -const ClawhubController = require('../app/controller/clawhub'); +const SkillsRegistryController = require('../app/controller/skillsRegistry'); function createMockCtx(query = {}, params = {}, body = {}, files = []) { const ctx = { @@ -33,7 +33,7 @@ function createMockCtx(query = {}, params = {}, body = {}, files = []) { // Helper to build a controller with mocked service function buildController(serviceMethods) { - const controller = Object.create(ClawhubController.prototype); + const controller = Object.create(SkillsRegistryController.prototype); const skillLikeService = Object.create(require('../app/service/skillLike').prototype); skillLikeService.resolveClientIp = () => '127.0.0.1'; skillLikeService.like = async () => ({ liked: true, likeCount: 1 }); @@ -41,7 +41,7 @@ function buildController(serviceMethods) { controller.ctx = createMockCtx(); controller.ctx.service = { - clawhub: serviceMethods, + skillsRegistry: serviceMethods, skillLike: skillLikeService, }; return controller; From a044ae45ac8fcd6cef814b0fb2a891384eb19677 Mon Sep 17 00:00:00 2001 From: huaiju Date: Wed, 17 Jun 2026 17:02:10 +0800 Subject: [PATCH 11/13] chore: apply PR review fixes for version and SkillCard styles Revert accidental version bump, rename SkillCard styles to style.scss, and remove internal plan artifact. Co-authored-by: Cursor --- app/web/components/skills/SkillCard.tsx | 2 + .../skills/{SkillCard.scss => style.scss} | 0 app/web/pages/skills/index.tsx | 1 - .../2026-06-08-fingerprint-file-contract.md | 42 ------------------- 4 files changed, 2 insertions(+), 43 deletions(-) rename app/web/components/skills/{SkillCard.scss => style.scss} (100%) delete mode 100644 docs/superpowers/plans/2026-06-08-fingerprint-file-contract.md diff --git a/app/web/components/skills/SkillCard.tsx b/app/web/components/skills/SkillCard.tsx index 08c28485..e5416ea8 100644 --- a/app/web/components/skills/SkillCard.tsx +++ b/app/web/components/skills/SkillCard.tsx @@ -4,6 +4,8 @@ import { Card, Checkbox, Tag } from 'antd'; import type { SkillItem } from '@/pages/skills/types'; +import './style.scss'; + interface SkillCardProps { skill: SkillItem; onClick: (skill: SkillItem) => void; diff --git a/app/web/components/skills/SkillCard.scss b/app/web/components/skills/style.scss similarity index 100% rename from app/web/components/skills/SkillCard.scss rename to app/web/components/skills/style.scss diff --git a/app/web/pages/skills/index.tsx b/app/web/pages/skills/index.tsx index d55eac0a..2810e375 100644 --- a/app/web/pages/skills/index.tsx +++ b/app/web/pages/skills/index.tsx @@ -28,7 +28,6 @@ import { import { API } from '@/api'; import { SkillItem, SkillListResponse } from './types'; import { SkillCard } from '@/components/skills/SkillCard'; -import '@/components/skills/SkillCard.scss'; import './style.scss'; const { Search } = Input; diff --git a/docs/superpowers/plans/2026-06-08-fingerprint-file-contract.md b/docs/superpowers/plans/2026-06-08-fingerprint-file-contract.md deleted file mode 100644 index 5165ecc0..00000000 --- a/docs/superpowers/plans/2026-06-08-fingerprint-file-contract.md +++ /dev/null @@ -1,42 +0,0 @@ -# Fingerprint File Contract Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make Doraemon's server-side skill fingerprint use the same text-file set as `dt-skill`, and ensure schema migration runs before `/api/v1/resolve` queries the skill model. - -**Architecture:** Keep the existing path-aware SHA256 formula. Add a focused server-side predicate that mirrors the CLI text-file contract: exclude hidden path segments, include allowlisted text extensions, and include extensionless files only when stored as non-binary. Call storage readiness before the first `SkillsItem` query in `resolveFingerprint`. - -**Tech Stack:** Egg.js service, Sequelize models, Node.js `crypto`, Node.js test runner. - ---- - -### Task 1: Lock the fingerprint file-set contract - -**Files:** -- Modify: `test/clawhub-contract.test.js` -- Modify: `app/service/clawhub.js` - -- [ ] Add a failing contract test proving binary assets, hidden paths, and unsupported extensions do not change the fingerprint, while extensionless text files do. -- [ ] Run `fnm exec --using=18 node --test test/clawhub-contract.test.js` and confirm the new assertion fails. -- [ ] Add a server-side `shouldIncludeFingerprintFile` predicate and filter records before hashing. -- [ ] Re-run the contract test and confirm it passes. - -### Task 2: Run migration before resolve queries - -**Files:** -- Modify: `test/clawhub-contract.test.js` -- Modify: `app/service/clawhub.js` - -- [ ] Add a failing test recording `migrate` before `query`. -- [ ] Run the contract test and confirm the observed order is currently `query`. -- [ ] Call `ensureStorageReady()` at the start of `resolveFingerprint`. -- [ ] Re-run the contract test and confirm `migrate, query`. - -### Task 3: Regression verification - -**Files:** -- Verify only. - -- [ ] Run all Clawhub and Skills backend tests under Node 18. -- [ ] Run `git diff --check`. -- [ ] Review the final diff and confirm P2 sorting code is unchanged. From 9698930b796656e6341e5121f0dba89ce39b41b8 Mon Sep 17 00:00:00 2001 From: huaiju Date: Thu, 18 Jun 2026 10:06:28 +0800 Subject: [PATCH 12/13] style: drop satisfies in github.ts and run prettier across repo prettier 2.7.1 cannot parse the satisfies operator, which broke the prettier CI step and silently masked downstream lint/typecheck/build failures. Replace satisfies with an explicit Promise return type, then format the rest of the repo to match the configured prettier rules. --- app/middleware/skillsStorageReady.js | 2 +- app/router.js | 18 +- app/service/skills.js | 72 +- app/service/skillsRegistry.js | 14 +- app/utils/command-runner.js | 8 +- .../detail/components/SkillDetailHero.tsx | 4 +- .../detail/components/SkillFileExplorer.tsx | 8 +- .../detail/components/SkillInstallPanel.tsx | 5 +- app/web/pages/skills/index.tsx | 1 - contracts/skill-fingerprint/extensions.json | 92 +- .../skill-fingerprint/golden-vectors.v1.json | 86 +- contracts/skill-fingerprint/index.d.ts | 4 +- contracts/skill-fingerprint/index.js | 5 +- dt-skill/bin/dt-skill.js | 2 +- dt-skill/package.json | 122 +- dt-skill/src/cli.test.ts | 68 +- dt-skill/src/cli.ts | 635 +++---- dt-skill/src/cli/agents.test.ts | 118 +- dt-skill/src/cli/agents.ts | 62 +- dt-skill/src/cli/buildInfo.ts | 140 +- dt-skill/src/cli/commands/delete.test.ts | 220 +-- dt-skill/src/cli/commands/delete.ts | 238 +-- dt-skill/src/cli/commands/github.test.ts | 761 ++++---- dt-skill/src/cli/commands/github.ts | 666 +++---- dt-skill/src/cli/commands/inspect.test.ts | 419 +++-- dt-skill/src/cli/commands/inspect.ts | 745 ++++---- dt-skill/src/cli/commands/publish.test.ts | 927 ++++----- dt-skill/src/cli/commands/publish.ts | 550 +++--- .../src/cli/commands/skills.install.test.ts | 579 +++--- dt-skill/src/cli/commands/skills.test.ts | 1577 ++++++++-------- dt-skill/src/cli/commands/skills.ts | 1649 +++++++++-------- dt-skill/src/cli/commands/star.ts | 64 +- dt-skill/src/cli/commands/unstar.ts | 66 +- dt-skill/src/cli/helpStyle.ts | 52 +- .../src/cli/prompts/search-multiselect.ts | 660 +++---- dt-skill/src/cli/registry.test.ts | 134 +- dt-skill/src/cli/registry.ts | 52 +- dt-skill/src/cli/scanSkills.test.ts | 92 +- dt-skill/src/cli/scanSkills.ts | 70 +- dt-skill/src/cli/slug.ts | 22 +- dt-skill/src/cli/types.ts | 20 +- dt-skill/src/cli/ui.test.ts | 106 +- dt-skill/src/cli/ui.ts | 226 +-- dt-skill/src/config.test.ts | 120 +- dt-skill/src/config.ts | 94 +- dt-skill/src/discovery.test.ts | 130 +- dt-skill/src/discovery.ts | 30 +- dt-skill/src/homedir.ts | 32 +- dt-skill/src/http.bun.test.ts | 338 ++-- dt-skill/src/http.test.ts | 740 ++++---- dt-skill/src/http.ts | 1310 ++++++------- dt-skill/src/schema/ark.ts | 42 +- dt-skill/src/schema/clawScanNote.ts | 12 +- dt-skill/src/schema/index.ts | 14 +- dt-skill/src/schema/license.ts | 8 +- dt-skill/src/schema/routes.ts | 36 +- dt-skill/src/schema/schemas.test.ts | 88 +- dt-skill/src/schema/schemas.ts | 825 ++++----- .../schema/skillFingerprintContract.test.ts | 60 +- .../src/schema/skillFingerprintContract.ts | 60 +- dt-skill/src/schema/textFiles.test.ts | 50 +- dt-skill/src/schema/textFiles.ts | 33 +- dt-skill/src/skills.test.ts | 498 ++--- dt-skill/src/skills.ts | 369 ++-- dt-skill/test-artifact/cli.artifact.test.ts | 5 +- dt-skill/test/cliCommandTestKit.ts | 141 +- dt-skill/test/runtimeStubs.ts | 94 +- dt-skill/tsconfig.json | 26 +- dt-skill/vitest.artifact.config.ts | 18 +- dt-skill/vitest.config.ts | 18 +- test/github-stars.test.js | 15 +- test/skills-import-package-name.test.js | 52 +- test/skills-install-key.test.js | 214 ++- test/skills-package-stars.test.js | 17 +- test/skills-registry-contract.test.js | 14 +- 75 files changed, 8588 insertions(+), 8276 deletions(-) diff --git a/app/middleware/skillsStorageReady.js b/app/middleware/skillsStorageReady.js index 0c904cef..1ec53770 100644 --- a/app/middleware/skillsStorageReady.js +++ b/app/middleware/skillsStorageReady.js @@ -3,7 +3,7 @@ module.exports = () => { return async function skillsStorageReady(ctx, next) { if (!storageReadyPromise) { - storageReadyPromise = ctx.service.skills.ensureStorageReady().catch(error => { + storageReadyPromise = ctx.service.skills.ensureStorageReady().catch((error) => { storageReadyPromise = null; throw error; }); diff --git a/app/router.js b/app/router.js index 6c3eb575..c3f6c8fd 100644 --- a/app/router.js +++ b/app/router.js @@ -171,18 +171,30 @@ module.exports = (app) => { app.get('/api/v1/search', skillsStorageReady, app.controller.skillsRegistry.search); app.get('/api/v1/skills', skillsStorageReady, app.controller.skillsRegistry.list); app.get('/api/v1/skills/:slug', skillsStorageReady, app.controller.skillsRegistry.detail); - app.get('/api/v1/skills/:slug/versions', skillsStorageReady, app.controller.skillsRegistry.versions); + app.get( + '/api/v1/skills/:slug/versions', + skillsStorageReady, + app.controller.skillsRegistry.versions + ); app.get( '/api/v1/skills/:slug/versions/:version', skillsStorageReady, app.controller.skillsRegistry.versionDetail ); - app.get('/api/v1/skills/:slug/file', skillsStorageReady, app.controller.skillsRegistry.fileContent); + app.get( + '/api/v1/skills/:slug/file', + skillsStorageReady, + app.controller.skillsRegistry.fileContent + ); app.get('/api/v1/download', skillsStorageReady, app.controller.skillsRegistry.download); app.get('/api/v1/resolve', skillsStorageReady, app.controller.skillsRegistry.resolve); app.post('/api/v1/skills', skillsStorageReady, app.controller.skillsRegistry.publish); app.delete('/api/v1/skills/:slug', skillsStorageReady, app.controller.skillsRegistry.delete); - app.post('/api/v1/skills/:slug/undelete', skillsStorageReady, app.controller.skillsRegistry.undelete); + app.post( + '/api/v1/skills/:slug/undelete', + skillsStorageReady, + app.controller.skillsRegistry.undelete + ); app.post('/api/v1/stars/:slug', skillsStorageReady, app.controller.skillsRegistry.star); app.delete('/api/v1/stars/:slug', skillsStorageReady, app.controller.skillsRegistry.unstar); diff --git a/app/service/skills.js b/app/service/skills.js index 7a984dc3..f1396350 100644 --- a/app/service/skills.js +++ b/app/service/skills.js @@ -456,7 +456,10 @@ class SkillsService extends Service { childFiles.forEach((row) => { const safeRelativePath = this.normalizeRelativePath(row.file_path); const zipPath = path.posix.join(rootFolder, childFolder, safeRelativePath); - const buffer = this.decodeStoredFileContent(row.content, Boolean(row.is_binary)); + const buffer = this.decodeStoredFileContent( + row.content, + Boolean(row.is_binary) + ); zip.addFile(zipPath, buffer); }); } @@ -633,9 +636,12 @@ class SkillsService extends Service { } buildSkillSlug(sourceMeta, relativeSkillPath, skillName, usedSlugs = new Set()) { - const isUpload = sourceMeta && (sourceMeta.repoHost === 'upload' || sourceMeta.sourceType === 'upload'); + const isUpload = + sourceMeta && (sourceMeta.repoHost === 'upload' || sourceMeta.sourceType === 'upload'); const relativeKey = String( - (relativeSkillPath && relativeSkillPath !== '.') ? relativeSkillPath : (skillName || 'skill') + relativeSkillPath && relativeSkillPath !== '.' + ? relativeSkillPath + : skillName || 'skill' ).replace(/\\/g, '/'); let base; @@ -1140,7 +1146,13 @@ class SkillsService extends Service { cloneArgs.push(parsedSource.cloneUrl, targetDir); try { - await this.commandRunner.runCommand('git', cloneArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env); + await this.commandRunner.runCommand( + 'git', + cloneArgs, + GIT_COMMAND_TIMEOUT_MS, + process.cwd(), + env + ); } catch (error) { if (!parsedSource.ref) { throw error; @@ -1155,7 +1167,13 @@ class SkillsService extends Service { parsedSource.cloneUrl, targetDir, ]; - await this.commandRunner.runCommand('git', fallbackArgs, GIT_COMMAND_TIMEOUT_MS, process.cwd(), env); + await this.commandRunner.runCommand( + 'git', + fallbackArgs, + GIT_COMMAND_TIMEOUT_MS, + process.cwd(), + env + ); } } @@ -1680,12 +1698,7 @@ class SkillsService extends Service { const tempUsedSlugs = new Set(); const excludeSlugs = skillRecords.map((record) => - this.buildSkillSlug( - parsedSource, - record.sourcePath, - record.name, - tempUsedSlugs - ) + this.buildSkillSlug(parsedSource, record.sourcePath, record.name, tempUsedSlugs) ); await this.assertSkillNamesUnique( @@ -1993,7 +2006,12 @@ class SkillsService extends Service { }; } - async persistSkillsForSource(sourceId, sourceMeta, skillRecords = [], preferredPackageName = '') { + async persistSkillsForSource( + sourceId, + sourceMeta, + skillRecords = [], + preferredPackageName = '' + ) { const { SkillsItem, SkillsFile } = this.app.model; const { Op } = this.app.Sequelize; const repoStars = await this.fetchStarsBySourceRepo(sourceMeta.sourceRepo); @@ -2034,27 +2052,30 @@ class SkillsService extends Service { const parts = (sourceMeta.repoPath || '').split('/'); parentName = parts[parts.length - 1] || 'git-skills-package'; } - parentSlug = this.buildSkillSlug( - sourceMeta, - '.', - parentName, - usedSlugs - ); + parentSlug = this.buildSkillSlug(sourceMeta, '.', parentName, usedSlugs); const parentPayload = { source_id: sourceId, slug: parentSlug, name: parentName, - description: `技能包,包含以下子技能:\n${skillRecords.map((r) => `- **${r.name}**: ${r.description || ''}`).join('\n')}`, + description: `技能包,包含以下子技能:\n${skillRecords + .map((r) => `- **${r.name}**: ${r.description || ''}`) + .join('\n')}`, category: skillRecords[0].category || '通用', version: '1.0.0', - tags: JSON.stringify(Array.from(new Set(skillRecords.flatMap((r) => r.tags || [])))), - allowed_tools: JSON.stringify(Array.from(new Set(skillRecords.flatMap((r) => r.allowedTools || [])))), + tags: JSON.stringify( + Array.from(new Set(skillRecords.flatMap((r) => r.tags || []))) + ), + allowed_tools: JSON.stringify( + Array.from(new Set(skillRecords.flatMap((r) => r.allowedTools || []))) + ), stars: resolvedStars, updated_at_remote: new Date(), source_repo: sourceMeta.sourceRepo || '', source_path: '.', - skill_md: `# ${parentName}\n\n这是一个技能包,包含以下子技能:\n${skillRecords.map((r) => `- **${r.name}**: ${r.description || ''}`).join('\n')}`, + skill_md: `# ${parentName}\n\n这是一个技能包,包含以下子技能:\n${skillRecords + .map((r) => `- **${r.name}**: ${r.description || ''}`) + .join('\n')}`, install_command: '', file_count: skillRecords.reduce((acc, r) => acc + (r.files || []).length, 0), is_delete: 0, @@ -2116,7 +2137,11 @@ class SkillsService extends Service { transaction, }); - if (globalExisting && globalExisting.is_delete === 0 && globalExisting.name !== record.name) { + if ( + globalExisting && + globalExisting.is_delete === 0 && + globalExisting.name !== record.name + ) { this.ctx.throw(400, 'slug 已存在'); } @@ -2316,7 +2341,6 @@ class SkillsService extends Service { const basicToken = Buffer.from(`oauth2:${token}`).toString('base64'); return ['-c', `http.https://${host}/.extraHeader=Authorization: Basic ${basicToken}`]; } - } const installKeyModule = require('../utils/skill-install-key'); diff --git a/app/service/skillsRegistry.js b/app/service/skillsRegistry.js index 63ceae07..a80bfc7c 100644 --- a/app/service/skillsRegistry.js +++ b/app/service/skillsRegistry.js @@ -170,12 +170,14 @@ class SkillsRegistryService extends Service { isPackage: skill.is_package === 1, parentSlug: skill.parent_slug || null, }, - latestVersion: version ? { - version, - createdAt: updatedAt, - changelog: '', - license: null, - } : null, + latestVersion: version + ? { + version, + createdAt: updatedAt, + changelog: '', + license: null, + } + : null, owner: null, moderation: null, }; diff --git a/app/utils/command-runner.js b/app/utils/command-runner.js index f40c8d24..4086b31c 100644 --- a/app/utils/command-runner.js +++ b/app/utils/command-runner.js @@ -7,7 +7,13 @@ class CommandRunner { this.defaultTimeout = defaultTimeout; } - runCommand(command, args = [], timeout = this.defaultTimeout, cwd = process.cwd(), env = process.env) { + runCommand( + command, + args = [], + timeout = this.defaultTimeout, + cwd = process.cwd(), + env = process.env + ) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, diff --git a/app/web/pages/skills/detail/components/SkillDetailHero.tsx b/app/web/pages/skills/detail/components/SkillDetailHero.tsx index b6c8f4fb..cbaf593a 100644 --- a/app/web/pages/skills/detail/components/SkillDetailHero.tsx +++ b/app/web/pages/skills/detail/components/SkillDetailHero.tsx @@ -102,7 +102,9 @@ export const SkillDetailHero: React.FC = ({
📦 - 包含 {detail.children.length} 个子技能 + + 包含 {detail.children.length} 个子技能 +
{detail.children.map((child: SkillItem) => ( diff --git a/app/web/pages/skills/detail/components/SkillFileExplorer.tsx b/app/web/pages/skills/detail/components/SkillFileExplorer.tsx index 57642946..4836c640 100644 --- a/app/web/pages/skills/detail/components/SkillFileExplorer.tsx +++ b/app/web/pages/skills/detail/components/SkillFileExplorer.tsx @@ -91,7 +91,9 @@ export const SkillFileExplorer: React.FC = ({ > {String(node.title)} @@ -110,9 +112,7 @@ export const SkillFileExplorer: React.FC = ({ onFocusInstallPanel(); copyToClipboard( isInstallable ? skillInstallCommand : downloadCommand, - isInstallable - ? '技能安装命令已复制到剪贴板' - : '下载命令已复制到剪贴板' + isInstallable ? '技能安装命令已复制到剪贴板' : '下载命令已复制到剪贴板' ); }} > diff --git a/app/web/pages/skills/detail/components/SkillInstallPanel.tsx b/app/web/pages/skills/detail/components/SkillInstallPanel.tsx index a2a099b9..52f13f32 100644 --- a/app/web/pages/skills/detail/components/SkillInstallPanel.tsx +++ b/app/web/pages/skills/detail/components/SkillInstallPanel.tsx @@ -274,10 +274,7 @@ export const SkillInstallPanel: React.FC = ({ onClick={() => history.push(browseMarketPath)} > 浏览市场 - + )} diff --git a/app/web/pages/skills/index.tsx b/app/web/pages/skills/index.tsx index 2810e375..ffd59e5c 100644 --- a/app/web/pages/skills/index.tsx +++ b/app/web/pages/skills/index.tsx @@ -51,7 +51,6 @@ const INITIAL_QUERY = { pageSize: 12, }; - const SkillsMarket: React.FC = ({ history }) => { const [loading, setLoading] = useState(false); const [skills, setSkills] = useState([]); diff --git a/contracts/skill-fingerprint/extensions.json b/contracts/skill-fingerprint/extensions.json index daf8e32f..33dae8d1 100644 --- a/contracts/skill-fingerprint/extensions.json +++ b/contracts/skill-fingerprint/extensions.json @@ -1,48 +1,48 @@ [ - "md", - "mdx", - "txt", - "json", - "json5", - "yaml", - "yml", - "toml", - "js", - "cjs", - "mjs", - "ts", - "tsx", - "jsx", - "py", - "sh", - "ps1", - "psm1", - "psd1", - "r", - "rb", - "go", - "rs", - "swift", - "kt", - "java", - "cs", - "cpp", - "c", - "h", - "hpp", - "sql", - "csv", - "tsv", - "ini", - "cfg", - "conf", - "env", - "properties", - "dat", - "xml", - "html", - "css", - "scss", - "sass", - "svg" + "md", + "mdx", + "txt", + "json", + "json5", + "yaml", + "yml", + "toml", + "js", + "cjs", + "mjs", + "ts", + "tsx", + "jsx", + "py", + "sh", + "ps1", + "psm1", + "psd1", + "r", + "rb", + "go", + "rs", + "swift", + "kt", + "java", + "cs", + "cpp", + "c", + "h", + "hpp", + "sql", + "csv", + "tsv", + "ini", + "cfg", + "conf", + "env", + "properties", + "dat", + "xml", + "html", + "css", + "scss", + "sass", + "svg" ] diff --git a/contracts/skill-fingerprint/golden-vectors.v1.json b/contracts/skill-fingerprint/golden-vectors.v1.json index 05798966..cc8b46e5 100644 --- a/contracts/skill-fingerprint/golden-vectors.v1.json +++ b/contracts/skill-fingerprint/golden-vectors.v1.json @@ -1,40 +1,50 @@ { - "version": 1, - "cases": [ - { - "name": "basic-markdown-pair", - "files": [ - { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, - { "path": "README.md", "content": "hello", "encoding": "utf8" } - ], - "fingerprint": "0dca25768cadcc32a50ae7a7ef5a19887353bf96086d18cd40203bc1877560eb" - }, - { - "name": "text-file-set-filtering", - "files": [ - { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, - { "path": "README.md", "content": "hello", "encoding": "utf8" }, - { "path": "Makefile", "content": "build:\n\techo ok", "encoding": "utf8" }, - { "path": "notes.", "content": "trailing dot", "encoding": "utf8" }, - { "path": "assets/logo.png", "content": "aW1hZ2U=", "encoding": "base64", "isBinary": true }, - { "path": ".private/notes.md", "content": "secret", "encoding": "utf8" }, - { "path": "docs/archive.bin", "content": "ignored", "encoding": "utf8" }, - { "path": "binary-no-extension", "content": "AAE=", "encoding": "base64", "isBinary": true } - ] - }, - { - "name": "stored-ignore-files", - "ignoreFiles": [ - { "path": ".gitignore", "content": "ignored.md\nbuild/\n" }, - { "path": ".dt-skillignore", "content": "private.txt\n" } - ], - "files": [ - { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, - { "path": "ignored.md", "content": "ignored", "encoding": "utf8" }, - { "path": "build/output.md", "content": "ignored build", "encoding": "utf8" }, - { "path": "private.txt", "content": "ignored private", "encoding": "utf8" }, - { "path": "docs/kept.md", "content": "kept", "encoding": "utf8" } - ] - } - ] + "version": 1, + "cases": [ + { + "name": "basic-markdown-pair", + "files": [ + { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, + { "path": "README.md", "content": "hello", "encoding": "utf8" } + ], + "fingerprint": "0dca25768cadcc32a50ae7a7ef5a19887353bf96086d18cd40203bc1877560eb" + }, + { + "name": "text-file-set-filtering", + "files": [ + { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, + { "path": "README.md", "content": "hello", "encoding": "utf8" }, + { "path": "Makefile", "content": "build:\n\techo ok", "encoding": "utf8" }, + { "path": "notes.", "content": "trailing dot", "encoding": "utf8" }, + { + "path": "assets/logo.png", + "content": "aW1hZ2U=", + "encoding": "base64", + "isBinary": true + }, + { "path": ".private/notes.md", "content": "secret", "encoding": "utf8" }, + { "path": "docs/archive.bin", "content": "ignored", "encoding": "utf8" }, + { + "path": "binary-no-extension", + "content": "AAE=", + "encoding": "base64", + "isBinary": true + } + ] + }, + { + "name": "stored-ignore-files", + "ignoreFiles": [ + { "path": ".gitignore", "content": "ignored.md\nbuild/\n" }, + { "path": ".dt-skillignore", "content": "private.txt\n" } + ], + "files": [ + { "path": "SKILL.md", "content": "# demo", "encoding": "utf8" }, + { "path": "ignored.md", "content": "ignored", "encoding": "utf8" }, + { "path": "build/output.md", "content": "ignored build", "encoding": "utf8" }, + { "path": "private.txt", "content": "ignored private", "encoding": "utf8" }, + { "path": "docs/kept.md", "content": "kept", "encoding": "utf8" } + ] + } + ] } diff --git a/contracts/skill-fingerprint/index.d.ts b/contracts/skill-fingerprint/index.d.ts index 6e019ebd..746ba762 100644 --- a/contracts/skill-fingerprint/index.d.ts +++ b/contracts/skill-fingerprint/index.d.ts @@ -21,9 +21,7 @@ export function shouldIncludeFingerprintFile(options: { export function sha256Hex(bytes: Uint8Array | Buffer): string; -export function buildSkillFingerprint( - files: Array<{ path: string; sha256: string }> -): string; +export function buildSkillFingerprint(files: Array<{ path: string; sha256: string }>): string; export function buildSkillFingerprintFromStoredFiles( files: Array<{ diff --git a/contracts/skill-fingerprint/index.js b/contracts/skill-fingerprint/index.js index 35de34e2..d9c164e5 100644 --- a/contracts/skill-fingerprint/index.js +++ b/contracts/skill-fingerprint/index.js @@ -7,10 +7,7 @@ const extensions = require('./extensions.json'); const TEXT_FILE_EXTENSIONS = Object.freeze([...extensions]); const TEXT_FILE_EXTENSION_SET = new Set(TEXT_FILE_EXTENSIONS); -const FINGERPRINT_IGNORE_FILENAMES = Object.freeze([ - '.gitignore', - '.dt-skillignore', -]); +const FINGERPRINT_IGNORE_FILENAMES = Object.freeze(['.gitignore', '.dt-skillignore']); const TEXT_SAMPLE_BYTES = 4096; diff --git a/dt-skill/bin/dt-skill.js b/dt-skill/bin/dt-skill.js index c667116b..21e8382e 100755 --- a/dt-skill/bin/dt-skill.js +++ b/dt-skill/bin/dt-skill.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -import("../dist/cli.js"); +import('../dist/cli.js'); diff --git a/dt-skill/package.json b/dt-skill/package.json index 36a96d9a..20742a97 100644 --- a/dt-skill/package.json +++ b/dt-skill/package.json @@ -1,63 +1,63 @@ { - "name": "dt-skill", - "version": "0.18.3", - "description": "dt-skill CLI — install, update, search, and publish agent skills.", - "homepage": "https://github.com/TreeTreeDi/doraemon", - "bugs": { - "url": "https://github.com/TreeTreeDi/doraemon/issues" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/TreeTreeDi/doraemon.git", - "directory": "dt-skill" - }, - "bin": { - "dt-skill": "bin/dt-skill.js" - }, - "files": [ - "bin", - "dist", - "README.md", - "LICENSE" - ], - "type": "module", - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "node ./scripts/build.mjs", - "dev": "node --enable-source-maps dist/cli.js", - "prepublishOnly": "npm run build", - "test": "bun run test:src", - "test:artifact": "bun run build && vitest run -c vitest.artifact.config.ts", - "test:src": "node ./scripts/build.mjs && vitest run -c vitest.config.ts", - "verify": "bun run test:src && bun run verify:build && bun run test:artifact", - "verify:build": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@clack/prompts": "1.4.0", - "adm-zip": "^0.5.17", - "arktype": "2.2.0", - "commander": "14.0.3", - "fflate": "0.8.2", - "ignore": "7.0.5", - "json5": "2.2.3", - "mime": "4.1.0", - "ora": "9.4.0", - "p-retry": "8.0.0", - "picocolors": "^1.1.1", - "semver": "7.8.0", - "undici": "7.25.0" - }, - "devDependencies": { - "@types/adm-zip": "^0.5.8", - "@types/node": "25.7.0", - "@types/semver": "^7.7.1", - "typescript": "6.0.3", - "vitest": "^4.1.7" - }, - "engines": { - "node": ">=20" - } + "name": "dt-skill", + "version": "0.18.3", + "description": "dt-skill CLI — install, update, search, and publish agent skills.", + "homepage": "https://github.com/TreeTreeDi/doraemon", + "bugs": { + "url": "https://github.com/TreeTreeDi/doraemon/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TreeTreeDi/doraemon.git", + "directory": "dt-skill" + }, + "bin": { + "dt-skill": "bin/dt-skill.js" + }, + "files": [ + "bin", + "dist", + "README.md", + "LICENSE" + ], + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "node ./scripts/build.mjs", + "dev": "node --enable-source-maps dist/cli.js", + "prepublishOnly": "npm run build", + "test": "bun run test:src", + "test:artifact": "bun run build && vitest run -c vitest.artifact.config.ts", + "test:src": "node ./scripts/build.mjs && vitest run -c vitest.config.ts", + "verify": "bun run test:src && bun run verify:build && bun run test:artifact", + "verify:build": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@clack/prompts": "1.4.0", + "adm-zip": "^0.5.17", + "arktype": "2.2.0", + "commander": "14.0.3", + "fflate": "0.8.2", + "ignore": "7.0.5", + "json5": "2.2.3", + "mime": "4.1.0", + "ora": "9.4.0", + "p-retry": "8.0.0", + "picocolors": "^1.1.1", + "semver": "7.8.0", + "undici": "7.25.0" + }, + "devDependencies": { + "@types/adm-zip": "^0.5.8", + "@types/node": "25.7.0", + "@types/semver": "^7.7.1", + "typescript": "6.0.3", + "vitest": "^4.1.7" + }, + "engines": { + "node": ">=20" + } } diff --git a/dt-skill/src/cli.test.ts b/dt-skill/src/cli.test.ts index 336f9eb4..471edbef 100644 --- a/dt-skill/src/cli.test.ts +++ b/dt-skill/src/cli.test.ts @@ -1,38 +1,32 @@ -import { describe, expect, it, vi } from "vitest"; -import { Command } from "commander"; - -describe("install command argument parsing", () => { - it("accepts multiple skill slugs via variadic argument", async () => { - const program = new Command(); - const installAction = vi.fn(); - - program - .command("install ") - .description("Install skill(s)") - .action(installAction); - - await program.parseAsync(["node", "dt-skill", "install", "skill-a", "skill-b", "skill-c"]); - - expect(installAction).toHaveBeenCalledTimes(1); - const receivedSlugs = installAction.mock.calls[0][0]; - expect(Array.isArray(receivedSlugs)).toBe(true); - expect(receivedSlugs).toEqual(["skill-a", "skill-b", "skill-c"]); - }); - - it("accepts a single skill slug", async () => { - const program = new Command(); - const installAction = vi.fn(); - - program - .command("install ") - .description("Install skill(s)") - .action(installAction); - - await program.parseAsync(["node", "dt-skill", "install", "single-skill"]); - - expect(installAction).toHaveBeenCalledTimes(1); - const receivedSlugs = installAction.mock.calls[0][0]; - expect(Array.isArray(receivedSlugs)).toBe(true); - expect(receivedSlugs).toEqual(["single-skill"]); - }); +import { describe, expect, it, vi } from 'vitest'; +import { Command } from 'commander'; + +describe('install command argument parsing', () => { + it('accepts multiple skill slugs via variadic argument', async () => { + const program = new Command(); + const installAction = vi.fn(); + + program.command('install ').description('Install skill(s)').action(installAction); + + await program.parseAsync(['node', 'dt-skill', 'install', 'skill-a', 'skill-b', 'skill-c']); + + expect(installAction).toHaveBeenCalledTimes(1); + const receivedSlugs = installAction.mock.calls[0][0]; + expect(Array.isArray(receivedSlugs)).toBe(true); + expect(receivedSlugs).toEqual(['skill-a', 'skill-b', 'skill-c']); + }); + + it('accepts a single skill slug', async () => { + const program = new Command(); + const installAction = vi.fn(); + + program.command('install ').description('Install skill(s)').action(installAction); + + await program.parseAsync(['node', 'dt-skill', 'install', 'single-skill']); + + expect(installAction).toHaveBeenCalledTimes(1); + const receivedSlugs = installAction.mock.calls[0][0]; + expect(Array.isArray(receivedSlugs)).toBe(true); + expect(receivedSlugs).toEqual(['single-skill']); + }); }); diff --git a/dt-skill/src/cli.ts b/dt-skill/src/cli.ts index ca2d73ff..e5731902 100644 --- a/dt-skill/src/cli.ts +++ b/dt-skill/src/cli.ts @@ -1,353 +1,358 @@ #!/usr/bin/env node -import { stat } from "node:fs/promises"; -import { join, resolve } from "node:path"; -import { Command } from "commander"; -import { getCliBuildLabel, getCliVersion } from "./cli/buildInfo.js"; +import { stat } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { Command } from 'commander'; +import { getCliBuildLabel, getCliVersion } from './cli/buildInfo.js'; import { - cmdDeleteSkill, - cmdHideSkill, - cmdUndeleteSkill, - cmdUnhideSkill, -} from "./cli/commands/delete.js"; -import { cmdInspect } from "./cli/commands/inspect.js"; -import { cmdPublish } from "./cli/commands/publish.js"; + cmdDeleteSkill, + cmdHideSkill, + cmdUndeleteSkill, + cmdUnhideSkill, +} from './cli/commands/delete.js'; +import { cmdInspect } from './cli/commands/inspect.js'; +import { cmdPublish } from './cli/commands/publish.js'; import { - cmdExplore, - cmdInstall, - cmdList, - cmdPin, - cmdSearch, - cmdUninstall, - cmdUnpin, - cmdUpdate, -} from "./cli/commands/skills.js"; -import { cmdStarSkill } from "./cli/commands/star.js"; -import { cmdUnstarSkill } from "./cli/commands/unstar.js"; -import { isAgentName, listAgentNames, resolveAgentWorkdir } from "./cli/agents.js"; -import { configureCommanderHelp, styleEnvBlock, styleTitle } from "./cli/helpStyle.js"; -import { DEFAULT_REGISTRY, DEFAULT_SITE } from "./cli/registry.js"; -import type { GlobalOpts } from "./cli/types.js"; -import { fail } from "./cli/ui.js"; + cmdExplore, + cmdInstall, + cmdList, + cmdPin, + cmdSearch, + cmdUninstall, + cmdUnpin, + cmdUpdate, +} from './cli/commands/skills.js'; +import { cmdStarSkill } from './cli/commands/star.js'; +import { cmdUnstarSkill } from './cli/commands/unstar.js'; +import { isAgentName, listAgentNames, resolveAgentWorkdir } from './cli/agents.js'; +import { configureCommanderHelp, styleEnvBlock, styleTitle } from './cli/helpStyle.js'; +import { DEFAULT_REGISTRY, DEFAULT_SITE } from './cli/registry.js'; +import type { GlobalOpts } from './cli/types.js'; +import { fail } from './cli/ui.js'; const CLAWSCAN_NOTE_HELP = - "This note gives ClawScan context for behavior that may otherwise look unusual, such as network access, native host access, or provider-specific credentials."; + 'This note gives ClawScan context for behavior that may otherwise look unusual, such as network access, native host access, or provider-specific credentials.'; const program = new Command() - .name("dt-skill") - .description( - `${styleTitle(`dt-skill CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock( - "install, update, search, and publish agent skills.", - )}`, - ) - .version(getCliVersion(), "-V, --cli-version", "Show CLI version") - .option("--workdir ", "Working directory (default: cwd)") - .option("--dir ", "Skills directory (relative to workdir, default: skills)") - .option("--site ", "Doraemon site URL for registry discovery") - .option("--registry ", "Registry API base URL") - .option("--agent ", `Target agent (${listAgentNames().join(", ")})`) - .option("--global", "Install skills to the global agent directory (requires --agent)") - .option("--no-input", "Disable prompts") - .showHelpAfterError() - .showSuggestionAfterError() - .addHelpText( - "after", - styleEnvBlock( - "\nEnv:\n DT_SKILL_SITE\n DT_SKILL_REGISTRY\n DT_SKILL_WORKDIR\n", - ), - ); + .name('dt-skill') + .description( + `${styleTitle(`dt-skill CLI ${getCliBuildLabel()}`)}\n${styleEnvBlock( + 'install, update, search, and publish agent skills.' + )}` + ) + .version(getCliVersion(), '-V, --cli-version', 'Show CLI version') + .option('--workdir ', 'Working directory (default: cwd)') + .option('--dir ', 'Skills directory (relative to workdir, default: skills)') + .option('--site ', 'Doraemon site URL for registry discovery') + .option('--registry ', 'Registry API base URL') + .option('--agent ', `Target agent (${listAgentNames().join(', ')})`) + .option('--global', 'Install skills to the global agent directory (requires --agent)') + .option('--no-input', 'Disable prompts') + .showHelpAfterError() + .showSuggestionAfterError() + .addHelpText( + 'after', + styleEnvBlock('\nEnv:\n DT_SKILL_SITE\n DT_SKILL_REGISTRY\n DT_SKILL_WORKDIR\n') + ); configureCommanderHelp(program); function registerCommand(parent: Command, path: readonly string[]) { - return parent.command(path.at(-1) ?? ""); + return parent.command(path.at(-1) ?? ''); } function registerCommandGroup(parent: Command, path: readonly string[]) { - return parent.command(path.at(-1) ?? ""); + return parent.command(path.at(-1) ?? ''); } async function resolveGlobalOpts(): Promise { - const raw = program.opts<{ - workdir?: string; - dir?: string; - site?: string; - registry?: string; - agent?: string; - global?: boolean; - }>(); - - const rawAgent = raw.agent?.trim(); - if (rawAgent && !isAgentName(rawAgent)) { - fail(`Unknown agent "${rawAgent}". Supported: ${listAgentNames().join(", ")}`); - } - const agentName: string | undefined = rawAgent; - - const isGlobal = raw.global ?? false; - if (isGlobal && !agentName) { - fail("--global requires --agent"); - } - - let workdir: string; - let dir: string; - - if (agentName) { - workdir = resolveAgentWorkdir(agentName as import("./cli/agents.js").AgentName, isGlobal); - dir = resolve(workdir, "skills"); - } else { - workdir = await resolveWorkdir(raw.workdir); - dir = resolve(workdir, raw.dir ?? "skills"); - } - - const site = raw.site ?? process.env.DT_SKILL_SITE ?? DEFAULT_SITE; - const registrySource = raw.registry - ? "cli" - : process.env.DT_SKILL_REGISTRY - ? "env" - : "default"; - const registry = raw.registry ?? process.env.DT_SKILL_REGISTRY ?? DEFAULT_REGISTRY; - return { workdir, dir, site, registry, registrySource, agent: agentName, globalScope: isGlobal, globalScopeExplicit: raw.global !== undefined }; + const raw = program.opts<{ + workdir?: string; + dir?: string; + site?: string; + registry?: string; + agent?: string; + global?: boolean; + }>(); + + const rawAgent = raw.agent?.trim(); + if (rawAgent && !isAgentName(rawAgent)) { + fail(`Unknown agent "${rawAgent}". Supported: ${listAgentNames().join(', ')}`); + } + const agentName: string | undefined = rawAgent; + + const isGlobal = raw.global ?? false; + if (isGlobal && !agentName) { + fail('--global requires --agent'); + } + + let workdir: string; + let dir: string; + + if (agentName) { + workdir = resolveAgentWorkdir(agentName as import('./cli/agents.js').AgentName, isGlobal); + dir = resolve(workdir, 'skills'); + } else { + workdir = await resolveWorkdir(raw.workdir); + dir = resolve(workdir, raw.dir ?? 'skills'); + } + + const site = raw.site ?? process.env.DT_SKILL_SITE ?? DEFAULT_SITE; + const registrySource = raw.registry ? 'cli' : process.env.DT_SKILL_REGISTRY ? 'env' : 'default'; + const registry = raw.registry ?? process.env.DT_SKILL_REGISTRY ?? DEFAULT_REGISTRY; + return { + workdir, + dir, + site, + registry, + registrySource, + agent: agentName, + globalScope: isGlobal, + globalScopeExplicit: raw.global !== undefined, + }; } function isInputAllowed() { - const globalFlags = program.opts<{ input?: boolean }>(); - return globalFlags.input !== false; + const globalFlags = program.opts<{ input?: boolean }>(); + return globalFlags.input !== false; } async function resolveWorkdir(explicit?: string) { - if (explicit?.trim()) return resolve(explicit.trim()); - const envWorkdir = process.env.DT_SKILL_WORKDIR?.trim(); - if (envWorkdir) return resolve(envWorkdir); + if (explicit?.trim()) return resolve(explicit.trim()); + const envWorkdir = process.env.DT_SKILL_WORKDIR?.trim(); + if (envWorkdir) return resolve(envWorkdir); - const cwd = resolve(process.cwd()); - if (await hasDtSkillMarker(cwd)) return cwd; - return cwd; + const cwd = resolve(process.cwd()); + if (await hasDtSkillMarker(cwd)) return cwd; + return cwd; } async function hasDtSkillMarker(workdir: string) { - const lockfile = join(workdir, ".dt-skill", "lock.json"); - if (await pathExists(lockfile)) return true; - const markerDir = join(workdir, ".dt-skill"); - return pathExists(markerDir); + const lockfile = join(workdir, '.dt-skill', 'lock.json'); + if (await pathExists(lockfile)) return true; + const markerDir = join(workdir, '.dt-skill'); + return pathExists(markerDir); } async function pathExists(path: string) { - try { - await stat(path); - return true; - } catch { - return false; - } + try { + await stat(path); + return true; + } catch { + return false; + } } -registerCommand(program, ["search"]) - .description("Vector search skills") - .argument("", "Query string") - .option("--limit ", "Max results", (value) => Number.parseInt(value, 10)) - .action(async (queryParts, options) => { - const opts = await resolveGlobalOpts(); - const query = queryParts.join(" ").trim(); - await cmdSearch(opts, query, options.limit); - }); - -registerCommand(program, ["install"]) - .description("Install skill(s) into /") - .argument("", "One or more skill slugs") - .option("--version ", "Version to install (single slug only)") - .option("--force", "Overwrite existing folders") - .action(async (slugs, options) => { - const opts = await resolveGlobalOpts(); - if (options.version && slugs.length > 1) { - fail("--version requires exactly one slug"); - } - await cmdInstall(opts, slugs, options.version, options.force); - }); - -registerCommand(program, ["update"]) - .description("Update installed skills") - .argument("[slug]", "Skill slug") - .option("--all", "Update all installed skills") - .option("--version ", "Update to specific version (single slug only)") - .option("--force", "Overwrite when local files do not match any version") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdUpdate(opts, slug, options, isInputAllowed()); - }); - -registerCommand(program, ["uninstall"]) - .description("Uninstall a skill") - .argument("", "Skill slug") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdUninstall(opts, slug, options, isInputAllowed()); - }); - -registerCommand(program, ["list"]) - .description("List installed skills (tracked and manually installed)") - .action(async () => { - const opts = await resolveGlobalOpts(); - await cmdList(opts); - }); - -registerCommand(program, ["pin"]) - .description("Pin an installed skill so update commands skip it") - .argument("", "Skill slug") - .option("--reason ", "Optional pin reason") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdPin(opts, slug, options); - }); - -registerCommand(program, ["unpin"]) - .description("Remove a skill pin so updates can change it again") - .argument("", "Skill slug") - .action(async (slug) => { - const opts = await resolveGlobalOpts(); - await cmdUnpin(opts, slug); - }); - -registerCommand(program, ["explore"]) - .description("Browse latest updated skills from the registry") - .option( - "--limit ", - "Number of skills to show (max 200)", - (value) => Number.parseInt(value, 10), - 25, - ) - .option( - "--sort ", - "Sort by newest, downloads, rating, installs, installsAllTime, or trending", - "newest", - ) - .option("--json", "Output JSON") - .action(async (options) => { - const opts = await resolveGlobalOpts(); - const limit = - typeof options.limit === "number" && Number.isFinite(options.limit) ? options.limit : 25; - await cmdExplore(opts, { limit, sort: options.sort, json: options.json }); - }); - -registerCommand(program, ["inspect"]) - .description("Fetch skill metadata and files without installing") - .argument("", "Skill slug") - .option("--version ", "Version to inspect") - .option("--tag ", "Tag to inspect (default: latest)") - .option("--versions", "List version history (first page)") - .option("--limit ", "Max versions to list (1-200)", (value) => Number.parseInt(value, 10)) - .option("--files", "List files for the selected version") - .option("--file ", "Fetch raw file content (text <= 200KB)") - .option("--json", "Output JSON") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdInspect(opts, slug, options); - }); - -registerCommand(program, ["publish"]) - .description("Legacy alias: publish a skill from folder") - .argument("", "Skill folder path") - .option("--slug ", "Skill slug") - .option("--name ", "Display name") - .option("--owner ", "Publish under an org/user publisher handle") - .option("--migrate-owner", "Move an existing skill to the selected owner when republishing") - .option("--version ", "Version (semver)") - .option("--fork-of ", "Mark as a fork of an existing skill") - .option("--changelog ", "Changelog text") - .option("--clawscan-note ", CLAWSCAN_NOTE_HELP) - .option("--tags ", "Comma-separated tags", "latest") - .option("--all", "Batch mode: upload all discovered skills without interactive selection") - .option("--category ", "Category for batch upload") - .action(async (folder, options) => { - const opts = await resolveGlobalOpts(); - await cmdPublish(opts, folder, options); - }); - -registerCommand(program, ["delete"]) - .description("Soft-delete one of your skills") - .argument("", "Skill slug") - .option("--reason ", "Moderation note/reason") - .option("--note ", "Alias for --reason") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdDeleteSkill(opts, slug, options, isInputAllowed()); - }); - -registerCommand(program, ["hide"]) - .description("Hide one of your skills") - .argument("", "Skill slug") - .option("--reason ", "Moderation note/reason") - .option("--note ", "Alias for --reason") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdHideSkill(opts, slug, options, isInputAllowed()); - }); - -registerCommand(program, ["undelete"]) - .description("Restore one of your hidden skills") - .argument("", "Skill slug") - .option("--reason ", "Moderation note/reason") - .option("--note ", "Alias for --reason") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdUndeleteSkill(opts, slug, options, isInputAllowed()); - }); - -registerCommand(program, ["unhide"]) - .description("Unhide one of your skills") - .argument("", "Skill slug") - .option("--reason ", "Moderation note/reason") - .option("--note ", "Alias for --reason") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdUnhideSkill(opts, slug, options, isInputAllowed()); - }); - -const skill = registerCommandGroup(program, ["skill"]).description("Manage published skills"); -registerCommand(skill, ["skill", "publish"]) - .description("Publish a skill from folder") - .argument("", "Skill folder path") - .option("--slug ", "Skill slug") - .option("--name ", "Display name") - .option("--owner ", "Publish under an org/user publisher handle") - .option("--migrate-owner", "Move an existing skill to the selected owner when republishing") - .option("--version ", "Version (semver)") - .option("--fork-of ", "Mark as a fork of an existing skill") - .option("--changelog ", "Changelog text") - .option("--clawscan-note ", CLAWSCAN_NOTE_HELP) - .option("--tags ", "Comma-separated tags", "latest") - .action(async (folder, options) => { - const opts = await resolveGlobalOpts(); - await cmdPublish(opts, folder, options); - }); - -registerCommand(program, ["star"]) - .description("Add a skill to your highlights") - .argument("", "Skill slug") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdStarSkill(opts, slug, options, isInputAllowed()); - }); - -registerCommand(program, ["unstar"]) - .description("Remove a skill from your highlights") - .argument("", "Skill slug") - .option("--yes", "Skip confirmation") - .action(async (slug, options) => { - const opts = await resolveGlobalOpts(); - await cmdUnstarSkill(opts, slug, options, isInputAllowed()); - }); +registerCommand(program, ['search']) + .description('Vector search skills') + .argument('', 'Query string') + .option('--limit ', 'Max results', (value) => Number.parseInt(value, 10)) + .action(async (queryParts, options) => { + const opts = await resolveGlobalOpts(); + const query = queryParts.join(' ').trim(); + await cmdSearch(opts, query, options.limit); + }); + +registerCommand(program, ['install']) + .description('Install skill(s) into /') + .argument('', 'One or more skill slugs') + .option('--version ', 'Version to install (single slug only)') + .option('--force', 'Overwrite existing folders') + .action(async (slugs, options) => { + const opts = await resolveGlobalOpts(); + if (options.version && slugs.length > 1) { + fail('--version requires exactly one slug'); + } + await cmdInstall(opts, slugs, options.version, options.force); + }); + +registerCommand(program, ['update']) + .description('Update installed skills') + .argument('[slug]', 'Skill slug') + .option('--all', 'Update all installed skills') + .option('--version ', 'Update to specific version (single slug only)') + .option('--force', 'Overwrite when local files do not match any version') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUpdate(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ['uninstall']) + .description('Uninstall a skill') + .argument('', 'Skill slug') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUninstall(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ['list']) + .description('List installed skills (tracked and manually installed)') + .action(async () => { + const opts = await resolveGlobalOpts(); + await cmdList(opts); + }); + +registerCommand(program, ['pin']) + .description('Pin an installed skill so update commands skip it') + .argument('', 'Skill slug') + .option('--reason ', 'Optional pin reason') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdPin(opts, slug, options); + }); + +registerCommand(program, ['unpin']) + .description('Remove a skill pin so updates can change it again') + .argument('', 'Skill slug') + .action(async (slug) => { + const opts = await resolveGlobalOpts(); + await cmdUnpin(opts, slug); + }); + +registerCommand(program, ['explore']) + .description('Browse latest updated skills from the registry') + .option( + '--limit ', + 'Number of skills to show (max 200)', + (value) => Number.parseInt(value, 10), + 25 + ) + .option( + '--sort ', + 'Sort by newest, downloads, rating, installs, installsAllTime, or trending', + 'newest' + ) + .option('--json', 'Output JSON') + .action(async (options) => { + const opts = await resolveGlobalOpts(); + const limit = + typeof options.limit === 'number' && Number.isFinite(options.limit) + ? options.limit + : 25; + await cmdExplore(opts, { limit, sort: options.sort, json: options.json }); + }); + +registerCommand(program, ['inspect']) + .description('Fetch skill metadata and files without installing') + .argument('', 'Skill slug') + .option('--version ', 'Version to inspect') + .option('--tag ', 'Tag to inspect (default: latest)') + .option('--versions', 'List version history (first page)') + .option('--limit ', 'Max versions to list (1-200)', (value) => Number.parseInt(value, 10)) + .option('--files', 'List files for the selected version') + .option('--file ', 'Fetch raw file content (text <= 200KB)') + .option('--json', 'Output JSON') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdInspect(opts, slug, options); + }); + +registerCommand(program, ['publish']) + .description('Legacy alias: publish a skill from folder') + .argument('', 'Skill folder path') + .option('--slug ', 'Skill slug') + .option('--name ', 'Display name') + .option('--owner ', 'Publish under an org/user publisher handle') + .option('--migrate-owner', 'Move an existing skill to the selected owner when republishing') + .option('--version ', 'Version (semver)') + .option('--fork-of ', 'Mark as a fork of an existing skill') + .option('--changelog ', 'Changelog text') + .option('--clawscan-note ', CLAWSCAN_NOTE_HELP) + .option('--tags ', 'Comma-separated tags', 'latest') + .option('--all', 'Batch mode: upload all discovered skills without interactive selection') + .option('--category ', 'Category for batch upload') + .action(async (folder, options) => { + const opts = await resolveGlobalOpts(); + await cmdPublish(opts, folder, options); + }); + +registerCommand(program, ['delete']) + .description('Soft-delete one of your skills') + .argument('', 'Skill slug') + .option('--reason ', 'Moderation note/reason') + .option('--note ', 'Alias for --reason') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdDeleteSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ['hide']) + .description('Hide one of your skills') + .argument('', 'Skill slug') + .option('--reason ', 'Moderation note/reason') + .option('--note ', 'Alias for --reason') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdHideSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ['undelete']) + .description('Restore one of your hidden skills') + .argument('', 'Skill slug') + .option('--reason ', 'Moderation note/reason') + .option('--note ', 'Alias for --reason') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUndeleteSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ['unhide']) + .description('Unhide one of your skills') + .argument('', 'Skill slug') + .option('--reason ', 'Moderation note/reason') + .option('--note ', 'Alias for --reason') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUnhideSkill(opts, slug, options, isInputAllowed()); + }); + +const skill = registerCommandGroup(program, ['skill']).description('Manage published skills'); +registerCommand(skill, ['skill', 'publish']) + .description('Publish a skill from folder') + .argument('', 'Skill folder path') + .option('--slug ', 'Skill slug') + .option('--name ', 'Display name') + .option('--owner ', 'Publish under an org/user publisher handle') + .option('--migrate-owner', 'Move an existing skill to the selected owner when republishing') + .option('--version ', 'Version (semver)') + .option('--fork-of ', 'Mark as a fork of an existing skill') + .option('--changelog ', 'Changelog text') + .option('--clawscan-note ', CLAWSCAN_NOTE_HELP) + .option('--tags ', 'Comma-separated tags', 'latest') + .action(async (folder, options) => { + const opts = await resolveGlobalOpts(); + await cmdPublish(opts, folder, options); + }); + +registerCommand(program, ['star']) + .description('Add a skill to your highlights') + .argument('', 'Skill slug') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdStarSkill(opts, slug, options, isInputAllowed()); + }); + +registerCommand(program, ['unstar']) + .description('Remove a skill from your highlights') + .argument('', 'Skill slug') + .option('--yes', 'Skip confirmation') + .action(async (slug, options) => { + const opts = await resolveGlobalOpts(); + await cmdUnstarSkill(opts, slug, options, isInputAllowed()); + }); program.action(async () => { - program.outputHelp(); - process.exitCode = 0; + program.outputHelp(); + process.exitCode = 0; }); void program.parseAsync(process.argv).catch((error) => { - const message = error instanceof Error ? error.message : String(error); - console.error(`Error: ${message}`); - process.exit(1); + const message = error instanceof Error ? error.message : String(error); + console.error(`Error: ${message}`); + process.exit(1); }); diff --git a/dt-skill/src/cli/agents.test.ts b/dt-skill/src/cli/agents.test.ts index ec43322a..c7a75bc9 100644 --- a/dt-skill/src/cli/agents.test.ts +++ b/dt-skill/src/cli/agents.test.ts @@ -1,75 +1,75 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest'; import { - AGENTS, - getAgentLabel, - isAgentName, - listAgentNames, - resolveAgentWorkdir, -} from "./agents.js"; + AGENTS, + getAgentLabel, + isAgentName, + listAgentNames, + resolveAgentWorkdir, +} from './agents.js'; -describe("agents", () => { - describe("isAgentName", () => { - it("returns true for known agents", () => { - expect(isAgentName("claude-code")).toBe(true); - expect(isAgentName("codex")).toBe(true); - expect(isAgentName("cursor")).toBe(true); - }); +describe('agents', () => { + describe('isAgentName', () => { + it('returns true for known agents', () => { + expect(isAgentName('claude-code')).toBe(true); + expect(isAgentName('codex')).toBe(true); + expect(isAgentName('cursor')).toBe(true); + }); - it("returns false for unknown agents", () => { - expect(isAgentName("unknown")).toBe(false); - expect(isAgentName("")).toBe(false); + it('returns false for unknown agents', () => { + expect(isAgentName('unknown')).toBe(false); + expect(isAgentName('')).toBe(false); + }); }); - }); - describe("listAgentNames", () => { - it("returns all agent names", () => { - const names = listAgentNames(); - expect(names).toContain("claude-code"); - expect(names).toContain("codex"); - expect(names).toContain("cursor"); + describe('listAgentNames', () => { + it('returns all agent names', () => { + const names = listAgentNames(); + expect(names).toContain('claude-code'); + expect(names).toContain('codex'); + expect(names).toContain('cursor'); + }); }); - }); - describe("getAgentLabel", () => { - it("returns the label for known agents", () => { - expect(getAgentLabel("claude-code")).toBe("Claude Code"); - expect(getAgentLabel("codex")).toBe("Codex"); - expect(getAgentLabel("cursor")).toBe("Cursor"); + describe('getAgentLabel', () => { + it('returns the label for known agents', () => { + expect(getAgentLabel('claude-code')).toBe('Claude Code'); + expect(getAgentLabel('codex')).toBe('Codex'); + expect(getAgentLabel('cursor')).toBe('Cursor'); + }); }); - }); - describe("resolveAgentWorkdir", () => { - it("resolves project workdir", () => { - const dir = resolveAgentWorkdir("claude-code", false); - expect(dir).toMatch(/\.claude$/); - }); + describe('resolveAgentWorkdir', () => { + it('resolves project workdir', () => { + const dir = resolveAgentWorkdir('claude-code', false); + expect(dir).toMatch(/\.claude$/); + }); - it("resolves global workdir with tilde", () => { - const dir = resolveAgentWorkdir("claude-code", true); - expect(dir).not.toContain("~"); - expect(dir).toMatch(/\.claude$/); - }); + it('resolves global workdir with tilde', () => { + const dir = resolveAgentWorkdir('claude-code', true); + expect(dir).not.toContain('~'); + expect(dir).toMatch(/\.claude$/); + }); - it("resolves codex project workdir", () => { - const dir = resolveAgentWorkdir("codex", false); - expect(dir).toMatch(/\.codex$/); - }); + it('resolves codex project workdir', () => { + const dir = resolveAgentWorkdir('codex', false); + expect(dir).toMatch(/\.codex$/); + }); - it("resolves cursor global workdir", () => { - const dir = resolveAgentWorkdir("cursor", true); - expect(dir).not.toContain("~"); - expect(dir).toMatch(/\.cursor$/); + it('resolves cursor global workdir', () => { + const dir = resolveAgentWorkdir('cursor', true); + expect(dir).not.toContain('~'); + expect(dir).toMatch(/\.cursor$/); + }); }); - }); - describe("AGENTS config", () => { - it("has consistent structure for all agents", () => { - for (const [key, agent] of Object.entries(AGENTS)) { - expect(agent.name).toBe(key); - expect(agent.label).toBeTruthy(); - expect(agent.projectWorkdir).toBeTruthy(); - expect(agent.globalWorkdir).toMatch(/^~/); - } + describe('AGENTS config', () => { + it('has consistent structure for all agents', () => { + for (const [key, agent] of Object.entries(AGENTS)) { + expect(agent.name).toBe(key); + expect(agent.label).toBeTruthy(); + expect(agent.projectWorkdir).toBeTruthy(); + expect(agent.globalWorkdir).toMatch(/^~/); + } + }); }); - }); }); diff --git a/dt-skill/src/cli/agents.ts b/dt-skill/src/cli/agents.ts index 67f5cc60..ea2f3ba5 100644 --- a/dt-skill/src/cli/agents.ts +++ b/dt-skill/src/cli/agents.ts @@ -1,51 +1,51 @@ -import { join } from "node:path"; -import { resolveHome } from "../homedir.js"; +import { join } from 'node:path'; +import { resolveHome } from '../homedir.js'; export const AGENTS = { - "claude-code": { - name: "claude-code", - label: "Claude Code", - projectWorkdir: ".claude", - globalWorkdir: "~/.claude", - }, - codex: { - name: "codex", - label: "Codex", - projectWorkdir: ".codex", - globalWorkdir: "~/.codex", - }, - cursor: { - name: "cursor", - label: "Cursor", - projectWorkdir: ".cursor", - globalWorkdir: "~/.cursor", - }, + 'claude-code': { + name: 'claude-code', + label: 'Claude Code', + projectWorkdir: '.claude', + globalWorkdir: '~/.claude', + }, + codex: { + name: 'codex', + label: 'Codex', + projectWorkdir: '.codex', + globalWorkdir: '~/.codex', + }, + cursor: { + name: 'cursor', + label: 'Cursor', + projectWorkdir: '.cursor', + globalWorkdir: '~/.cursor', + }, } as const; export type AgentName = keyof typeof AGENTS; export function isAgentName(value: string): value is AgentName { - return value in AGENTS; + return value in AGENTS; } export function listAgentNames(): AgentName[] { - return Object.keys(AGENTS) as AgentName[]; + return Object.keys(AGENTS) as AgentName[]; } function resolveTilde(path: string): string { - if (path.startsWith("~/")) { - return join(resolveHome(), path.slice(2)); - } - return path; + if (path.startsWith('~/')) { + return join(resolveHome(), path.slice(2)); + } + return path; } export function resolveAgentWorkdir(agentName: AgentName, isGlobal: boolean): string { - const agent = AGENTS[agentName]; - if (!agent) throw new Error(`Unknown agent: ${agentName}`); - const raw = isGlobal ? agent.globalWorkdir : agent.projectWorkdir; - return resolveTilde(raw); + const agent = AGENTS[agentName]; + if (!agent) throw new Error(`Unknown agent: ${agentName}`); + const raw = isGlobal ? agent.globalWorkdir : agent.projectWorkdir; + return resolveTilde(raw); } export function getAgentLabel(agentName: AgentName): string { - return AGENTS[agentName]?.label ?? agentName; + return AGENTS[agentName]?.label ?? agentName; } diff --git a/dt-skill/src/cli/buildInfo.ts b/dt-skill/src/cli/buildInfo.ts index d06884be..18c888f9 100644 --- a/dt-skill/src/cli/buildInfo.ts +++ b/dt-skill/src/cli/buildInfo.ts @@ -1,94 +1,94 @@ -import { existsSync, readFileSync, statSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; +import { existsSync, readFileSync, statSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; type PackageJson = { version?: string }; function readPackageVersion() { - try { - const path = join(dirname(fileURLToPath(import.meta.url)), "../../package.json"); - const raw = readFileSync(path, "utf8"); - const pkg = JSON.parse(raw) as PackageJson; - return typeof pkg.version === "string" ? pkg.version : "0.0.0"; - } catch { - return "0.0.0"; - } + try { + const path = join(dirname(fileURLToPath(import.meta.url)), '../../package.json'); + const raw = readFileSync(path, 'utf8'); + const pkg = JSON.parse(raw) as PackageJson; + return typeof pkg.version === 'string' ? pkg.version : '0.0.0'; + } catch { + return '0.0.0'; + } } function shortCommit(value: string) { - const trimmed = value.trim(); - if (!trimmed) return null; - if (trimmed.length <= 8) return trimmed; - return trimmed.slice(0, 8); + const trimmed = value.trim(); + if (!trimmed) return null; + if (trimmed.length <= 8) return trimmed; + return trimmed.slice(0, 8); } function getCliCommit() { - const candidates = [ - process.env.DT_SKILL_COMMIT, - process.env.VERCEL_GIT_COMMIT_SHA, - process.env.GITHUB_SHA, - process.env.COMMIT_SHA, - ]; - for (const candidate of candidates) { - if (!candidate) continue; - const short = shortCommit(candidate); - if (short) return short; - } - return readGitCommitFromCwd(); + const candidates = [ + process.env.DT_SKILL_COMMIT, + process.env.VERCEL_GIT_COMMIT_SHA, + process.env.GITHUB_SHA, + process.env.COMMIT_SHA, + ]; + for (const candidate of candidates) { + if (!candidate) continue; + const short = shortCommit(candidate); + if (short) return short; + } + return readGitCommitFromCwd(); } export function getCliVersion() { - return readPackageVersion(); + return readPackageVersion(); } export function getCliBuildLabel() { - const version = getCliVersion(); - const commit = getCliCommit(); - return commit ? `v${version} (${commit})` : `v${version}`; + const version = getCliVersion(); + const commit = getCliCommit(); + return commit ? `v${version} (${commit})` : `v${version}`; } function readGitCommitFromCwd() { - try { - const gitDir = findGitDir(process.cwd()); - if (!gitDir) return null; - const headPath = join(gitDir, "HEAD"); - if (!existsSync(headPath)) return null; - const head = readFileSync(headPath, "utf8").trim(); - if (!head) return null; - if (!head.startsWith("ref:")) return shortCommit(head); - const ref = head.replace(/^ref:\s*/, "").trim(); - if (!ref) return null; - const refPath = join(gitDir, ref); - if (!existsSync(refPath)) return null; - const sha = readFileSync(refPath, "utf8").trim(); - return shortCommit(sha); - } catch { - return null; - } + try { + const gitDir = findGitDir(process.cwd()); + if (!gitDir) return null; + const headPath = join(gitDir, 'HEAD'); + if (!existsSync(headPath)) return null; + const head = readFileSync(headPath, 'utf8').trim(); + if (!head) return null; + if (!head.startsWith('ref:')) return shortCommit(head); + const ref = head.replace(/^ref:\s*/, '').trim(); + if (!ref) return null; + const refPath = join(gitDir, ref); + if (!existsSync(refPath)) return null; + const sha = readFileSync(refPath, 'utf8').trim(); + return shortCommit(sha); + } catch { + return null; + } } function findGitDir(start: string) { - let current = resolve(start); - for (;;) { - const dotGit = join(current, ".git"); - if (existsSync(dotGit)) { - try { - const stat = statSync(dotGit); - if (stat.isDirectory()) return dotGit; - } catch { - // ignore - } - try { - const content = readFileSync(dotGit, "utf8").trim(); - const match = content.match(/^gitdir:\s*(.+)$/); - if (match?.[1]) return resolve(current, match[1]); - } catch { - return dotGit; - } - return dotGit; + let current = resolve(start); + for (;;) { + const dotGit = join(current, '.git'); + if (existsSync(dotGit)) { + try { + const stat = statSync(dotGit); + if (stat.isDirectory()) return dotGit; + } catch { + // ignore + } + try { + const content = readFileSync(dotGit, 'utf8').trim(); + const match = content.match(/^gitdir:\s*(.+)$/); + if (match?.[1]) return resolve(current, match[1]); + } catch { + return dotGit; + } + return dotGit; + } + const parent = resolve(current, '..'); + if (parent === current) return null; + current = parent; } - const parent = resolve(current, ".."); - if (parent === current) return null; - current = parent; - } } diff --git a/dt-skill/src/cli/commands/delete.test.ts b/dt-skill/src/cli/commands/delete.test.ts index 5899d6b9..fae9a390 100644 --- a/dt-skill/src/cli/commands/delete.test.ts +++ b/dt-skill/src/cli/commands/delete.test.ts @@ -1,132 +1,134 @@ /* @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { - createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from '../../../test/cliCommandTestKit.js'; const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks(); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../ui.js", () => uiMocks.moduleFactory()); +vi.mock('../registry.js', () => registryMocks.moduleFactory()); +vi.mock('../../http.js', () => httpMocks.moduleFactory()); +vi.mock('../ui.js', () => uiMocks.moduleFactory()); -const { cmdDeleteSkill, cmdHideSkill, cmdUndeleteSkill, cmdUnhideSkill } = await import("./delete"); +const { cmdDeleteSkill, cmdHideSkill, cmdUndeleteSkill, cmdUnhideSkill } = await import('./delete'); afterEach(() => { - vi.clearAllMocks(); + vi.clearAllMocks(); }); -describe("delete/undelete", () => { - it("requires --yes when input is disabled", async () => { - await expect(cmdDeleteSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); - await expect(cmdUndeleteSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); - await expect(cmdHideSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); - await expect(cmdUnhideSkill(makeGlobalOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); - }); +describe('delete/undelete', () => { + it('requires --yes when input is disabled', async () => { + await expect(cmdDeleteSkill(makeGlobalOpts(), 'demo', {}, false)).rejects.toThrow(/--yes/i); + await expect(cmdUndeleteSkill(makeGlobalOpts(), 'demo', {}, false)).rejects.toThrow( + /--yes/i + ); + await expect(cmdHideSkill(makeGlobalOpts(), 'demo', {}, false)).rejects.toThrow(/--yes/i); + await expect(cmdUnhideSkill(makeGlobalOpts(), 'demo', {}, false)).rejects.toThrow(/--yes/i); + }); - it("calls delete endpoint with --yes", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - await cmdDeleteSkill(makeGlobalOpts(), "demo", { yes: true }, false); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.not.objectContaining({ token: expect.anything() }), - expect.anything(), - ); - }); + it('calls delete endpoint with --yes', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdDeleteSkill(makeGlobalOpts(), 'demo', { yes: true }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.not.objectContaining({ token: expect.anything() }), + expect.anything() + ); + }); - it("prints the slug reservation expiry returned by delete", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - ok: true, - slugReservedUntil: 1_700_086_400_000, + it('prints the slug reservation expiry returned by delete', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + ok: true, + slugReservedUntil: 1_700_086_400_000, + }); + await cmdDeleteSkill(makeGlobalOpts(), 'demo', { yes: true }, false); + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + 'OK. Deleted demo. Slug reserved until 2023-11-15T22:13:20.000Z' + ); }); - await cmdDeleteSkill(makeGlobalOpts(), "demo", { yes: true }, false); - expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( - "OK. Deleted demo. Slug reserved until 2023-11-15T22:13:20.000Z", - ); - }); - it("passes a moderation reason on delete", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - await cmdDeleteSkill(makeGlobalOpts(), "demo", { yes: true, reason: "legal hold" }, false); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "DELETE", - path: "/api/v1/skills/demo", - body: { reason: "legal hold" }, - }), - expect.anything(), - ); - }); + it('passes a moderation reason on delete', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdDeleteSkill(makeGlobalOpts(), 'demo', { yes: true, reason: 'legal hold' }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'DELETE', + path: '/api/v1/skills/demo', + body: { reason: 'legal hold' }, + }), + expect.anything() + ); + }); - it("supports --note as a reason alias", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - await cmdHideSkill(makeGlobalOpts(), "demo", { yes: true, note: "legal notice" }, false); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "DELETE", - path: "/api/v1/skills/demo", - body: { reason: "legal notice" }, - }), - expect.anything(), - ); - }); + it('supports --note as a reason alias', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdHideSkill(makeGlobalOpts(), 'demo', { yes: true, note: 'legal notice' }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'DELETE', + path: '/api/v1/skills/demo', + body: { reason: 'legal notice' }, + }), + expect.anything() + ); + }); - it("rejects conflicting reason aliases", async () => { - await expect( - cmdHideSkill( - makeGlobalOpts(), - "demo", - { yes: true, reason: "legal hold", note: "different" }, - false, - ), - ).rejects.toThrow(/only one/i); - }); + it('rejects conflicting reason aliases', async () => { + await expect( + cmdHideSkill( + makeGlobalOpts(), + 'demo', + { yes: true, reason: 'legal hold', note: 'different' }, + false + ) + ).rejects.toThrow(/only one/i); + }); - it("calls undelete endpoint with --yes", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - await cmdUndeleteSkill(makeGlobalOpts(), "demo", { yes: true }, false); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/undelete" }), - expect.anything(), - ); - }); + it('calls undelete endpoint with --yes', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdUndeleteSkill(makeGlobalOpts(), 'demo', { yes: true }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: 'POST', path: '/api/v1/skills/demo/undelete' }), + expect.anything() + ); + }); - it("passes a moderation reason on undelete", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); - await cmdUndeleteSkill(makeGlobalOpts(), "demo", { yes: true, reason: "reviewed" }, false); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - method: "POST", - path: "/api/v1/skills/demo/undelete", - body: { reason: "reviewed" }, - }), - expect.anything(), - ); - }); + it('passes a moderation reason on undelete', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ ok: true }); + await cmdUndeleteSkill(makeGlobalOpts(), 'demo', { yes: true, reason: 'reviewed' }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + method: 'POST', + path: '/api/v1/skills/demo/undelete', + body: { reason: 'reviewed' }, + }), + expect.anything() + ); + }); - it("supports hide/unhide aliases", async () => { - httpMocks.apiRequest.mockResolvedValue({ ok: true }); - await cmdHideSkill(makeGlobalOpts(), "demo", { yes: true }, false); - await cmdUnhideSkill(makeGlobalOpts(), "demo", { yes: true }, false); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ method: "DELETE", path: "/api/v1/skills/demo" }), - expect.anything(), - ); - expect(httpMocks.apiRequest).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ method: "POST", path: "/api/v1/skills/demo/undelete" }), - expect.anything(), - ); - }); + it('supports hide/unhide aliases', async () => { + httpMocks.apiRequest.mockResolvedValue({ ok: true }); + await cmdHideSkill(makeGlobalOpts(), 'demo', { yes: true }, false); + await cmdUnhideSkill(makeGlobalOpts(), 'demo', { yes: true }, false); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: 'DELETE', path: '/api/v1/skills/demo' }), + expect.anything() + ); + expect(httpMocks.apiRequest).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ method: 'POST', path: '/api/v1/skills/demo/undelete' }), + expect.anything() + ); + }); }); diff --git a/dt-skill/src/cli/commands/delete.ts b/dt-skill/src/cli/commands/delete.ts index 8051ace1..d1d4ee7b 100644 --- a/dt-skill/src/cli/commands/delete.ts +++ b/dt-skill/src/cli/commands/delete.ts @@ -1,162 +1,162 @@ -import { apiRequest } from "../../http.js"; -import { ApiRoutes, ApiV1DeleteResponseSchema, parseArk } from "../../schema/index.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; +import { apiRequest } from '../../http.js'; +import { ApiRoutes, ApiV1DeleteResponseSchema, parseArk } from '../../schema/index.js'; +import { getRegistry } from '../registry.js'; +import type { GlobalOpts } from '../types.js'; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'; type SkillActionLabels = { - verb: string; - progress: string; - past: string; - promptSuffix?: string; + verb: string; + progress: string; + past: string; + promptSuffix?: string; }; type SkillDeleteOptions = { - yes?: boolean; - reason?: string; - note?: string; + yes?: boolean; + reason?: string; + note?: string; }; const deleteLabels: SkillActionLabels = { - verb: "Delete", - progress: "Deleting", - past: "Deleted", - promptSuffix: "soft delete; owner slug reservation expires after 30 days", + verb: 'Delete', + progress: 'Deleting', + past: 'Deleted', + promptSuffix: 'soft delete; owner slug reservation expires after 30 days', }; const undeleteLabels: SkillActionLabels = { - verb: "Undelete", - progress: "Undeleting", - past: "Undeleted", - promptSuffix: "owner/moderator/admin", + verb: 'Undelete', + progress: 'Undeleting', + past: 'Undeleted', + promptSuffix: 'owner/moderator/admin', }; const hideLabels: SkillActionLabels = { - verb: "Hide", - progress: "Hiding", - past: "Hidden", - promptSuffix: "owner/moderator/admin", + verb: 'Hide', + progress: 'Hiding', + past: 'Hidden', + promptSuffix: 'owner/moderator/admin', }; const unhideLabels: SkillActionLabels = { - verb: "Unhide", - progress: "Unhiding", - past: "Unhidden", - promptSuffix: "owner/moderator/admin", + verb: 'Unhide', + progress: 'Unhiding', + past: 'Unhidden', + promptSuffix: 'owner/moderator/admin', }; export async function cmdDeleteSkill( - opts: GlobalOpts, - slugArg: string, - options: SkillDeleteOptions, - inputAllowed: boolean, - labels: SkillActionLabels = deleteLabels, + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean, + labels: SkillActionLabels = deleteLabels ) { - const slug = slugArg.trim().toLowerCase(); - if (!slug) fail("Slug required"); - const reason = normalizeReason(options); - const allowPrompt = isInteractive() && inputAllowed !== false; - - if (!options.yes) { - if (!allowPrompt) fail("Pass --yes (no input)"); - const ok = await promptConfirm(formatPrompt(labels, slug)); - if (!ok) return undefined; - } - - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`${labels.progress} ${slug}`); - try { - const result = await apiRequest( - registry, - { - method: "DELETE", - path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}`, - body: reason ? { reason } : undefined, - }, - ApiV1DeleteResponseSchema, - ); - const parsed = parseArk(ApiV1DeleteResponseSchema, result, "Delete response"); - spinner.succeed(`OK. ${labels.past} ${slug}${formatSlugReservation(parsed)}`); - return parsed; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail('Slug required'); + const reason = normalizeReason(options); + const allowPrompt = isInteractive() && inputAllowed !== false; + + if (!options.yes) { + if (!allowPrompt) fail('Pass --yes (no input)'); + const ok = await promptConfirm(formatPrompt(labels, slug)); + if (!ok) return undefined; + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`${labels.progress} ${slug}`); + try { + const result = await apiRequest( + registry, + { + method: 'DELETE', + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}`, + body: reason ? { reason } : undefined, + }, + ApiV1DeleteResponseSchema + ); + const parsed = parseArk(ApiV1DeleteResponseSchema, result, 'Delete response'); + spinner.succeed(`OK. ${labels.past} ${slug}${formatSlugReservation(parsed)}`); + return parsed; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } export async function cmdUndeleteSkill( - opts: GlobalOpts, - slugArg: string, - options: SkillDeleteOptions, - inputAllowed: boolean, - labels: SkillActionLabels = undeleteLabels, + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean, + labels: SkillActionLabels = undeleteLabels ) { - const slug = slugArg.trim().toLowerCase(); - if (!slug) fail("Slug required"); - const reason = normalizeReason(options); - const allowPrompt = isInteractive() && inputAllowed !== false; - - if (!options.yes) { - if (!allowPrompt) fail("Pass --yes (no input)"); - const ok = await promptConfirm(formatPrompt(labels, slug)); - if (!ok) return undefined; - } - - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`${labels.progress} ${slug}`); - try { - const result = await apiRequest( - registry, - { - method: "POST", - path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/undelete`, - body: reason ? { reason } : undefined, - }, - ApiV1DeleteResponseSchema, - ); - spinner.succeed(`OK. ${labels.past} ${slug}`); - return parseArk(ApiV1DeleteResponseSchema, result, "Undelete response"); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail('Slug required'); + const reason = normalizeReason(options); + const allowPrompt = isInteractive() && inputAllowed !== false; + + if (!options.yes) { + if (!allowPrompt) fail('Pass --yes (no input)'); + const ok = await promptConfirm(formatPrompt(labels, slug)); + if (!ok) return undefined; + } + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`${labels.progress} ${slug}`); + try { + const result = await apiRequest( + registry, + { + method: 'POST', + path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}/undelete`, + body: reason ? { reason } : undefined, + }, + ApiV1DeleteResponseSchema + ); + spinner.succeed(`OK. ${labels.past} ${slug}`); + return parseArk(ApiV1DeleteResponseSchema, result, 'Undelete response'); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } export async function cmdHideSkill( - opts: GlobalOpts, - slugArg: string, - options: SkillDeleteOptions, - inputAllowed: boolean, + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean ) { - return cmdDeleteSkill(opts, slugArg, options, inputAllowed, hideLabels); + return cmdDeleteSkill(opts, slugArg, options, inputAllowed, hideLabels); } export async function cmdUnhideSkill( - opts: GlobalOpts, - slugArg: string, - options: SkillDeleteOptions, - inputAllowed: boolean, + opts: GlobalOpts, + slugArg: string, + options: SkillDeleteOptions, + inputAllowed: boolean ) { - return cmdUndeleteSkill(opts, slugArg, options, inputAllowed, unhideLabels); + return cmdUndeleteSkill(opts, slugArg, options, inputAllowed, unhideLabels); } function normalizeReason(options: SkillDeleteOptions) { - const reason = options.reason?.trim(); - const note = options.note?.trim(); - if (reason && note && reason !== note) fail("Pass only one of --reason or --note"); - const value = reason || note; - if ((options.reason !== undefined || options.note !== undefined) && !value) { - fail("--reason cannot be empty"); - } - return value; + const reason = options.reason?.trim(); + const note = options.note?.trim(); + if (reason && note && reason !== note) fail('Pass only one of --reason or --note'); + const value = reason || note; + if ((options.reason !== undefined || options.note !== undefined) && !value) { + fail('--reason cannot be empty'); + } + return value; } function formatPrompt(labels: SkillActionLabels, slug: string) { - const suffix = labels.promptSuffix ? ` (${labels.promptSuffix})` : ""; - return `${labels.verb} ${slug}?${suffix}`; + const suffix = labels.promptSuffix ? ` (${labels.promptSuffix})` : ''; + return `${labels.verb} ${slug}?${suffix}`; } function formatSlugReservation(result: { slugReservedUntil?: number }) { - if (typeof result.slugReservedUntil !== "number") return ""; - return `. Slug reserved until ${new Date(result.slugReservedUntil).toISOString()}`; + if (typeof result.slugReservedUntil !== 'number') return ''; + return `. Slug reserved until ${new Date(result.slugReservedUntil).toISOString()}`; } diff --git a/dt-skill/src/cli/commands/github.test.ts b/dt-skill/src/cli/commands/github.test.ts index df222af6..bdba37ab 100644 --- a/dt-skill/src/cli/commands/github.test.ts +++ b/dt-skill/src/cli/commands/github.test.ts @@ -1,410 +1,425 @@ /* @vitest-environment node */ -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { zipSync } from "fflate"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { fetchGitHubSource, resolveLocalGitInfo, resolveSourceInput } from "./github"; +import { spawnSync } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { zipSync } from 'fflate'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { fetchGitHubSource, resolveLocalGitInfo, resolveSourceInput } from './github'; async function makeTmpDir() { - return await mkdtemp(join(tmpdir(), "dt-skill-github-test-")); + return await mkdtemp(join(tmpdir(), 'dt-skill-github-test-')); } function runGit(cwd: string, args: string[]) { - const result = spawnSync("git", ["-C", cwd, ...args], { - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }); - if (result.status !== 0) { - throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); - } - return result.stdout.trim(); + const result = spawnSync('git', ['-C', cwd, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + if (result.status !== 0) { + throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`); + } + return result.stdout.trim(); } afterEach(() => { - vi.restoreAllMocks(); + vi.restoreAllMocks(); }); function mockGitHubCommitLookup(validRefs: string[]) { - const originalFetch = globalThis.fetch; - const fetchMock = vi.fn(async (input) => { - const url = input instanceof Request ? input.url : input.toString(); - const match = url.match(/\/repos\/owner\/repo\/commits\/(.+)$/); - if (!match) { - throw new Error(`Unexpected fetch: ${url}`); - } - const ref = decodeURIComponent(match[1] ?? ""); - if (!validRefs.includes(ref)) { - return new Response("not found", { status: 404 }); - } - return new Response(JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }), { - status: 200, - headers: { "content-type": "application/json" }, + const originalFetch = globalThis.fetch; + const fetchMock = vi.fn(async (input) => { + const url = input instanceof Request ? input.url : input.toString(); + const match = url.match(/\/repos\/owner\/repo\/commits\/(.+)$/); + if (!match) { + throw new Error(`Unexpected fetch: ${url}`); + } + const ref = decodeURIComponent(match[1] ?? ''); + if (!validRefs.includes(ref)) { + return new Response('not found', { status: 404 }); + } + return new Response(JSON.stringify({ sha: '0123456789abcdef0123456789abcdef01234567' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); }); - }); - Object.defineProperty(globalThis, "fetch", { - value: fetchMock, - configurable: true, - writable: true, - }); - return () => { - Object.defineProperty(globalThis, "fetch", { - value: originalFetch, - configurable: true, - writable: true, + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, }); - }; + return () => { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + }; } -describe("github publish source helpers", () => { - it.each([ - [ - "owner/repo", - { - kind: "github", - owner: "owner", - repo: "repo", - path: ".", - url: "https://github.com/owner/repo", - }, - ], - [ - "owner/repo@v1.0.0", - { - kind: "github", - owner: "owner", - repo: "repo", - ref: "v1.0.0", - path: ".", - url: "https://github.com/owner/repo", - }, - ], - [ - "owner/repo@main", - { - kind: "github", - owner: "owner", - repo: "repo", - ref: "main", - path: ".", - url: "https://github.com/owner/repo", - }, - ], - [ - "https://github.com/owner/repo", - { - kind: "github", - owner: "owner", - repo: "repo", - path: ".", - url: "https://github.com/owner/repo", - }, - ], - [ - "https://github.com/owner/repo/tree/main", - { - kind: "github", - owner: "owner", - repo: "repo", - ref: "main", - path: ".", - url: "https://github.com/owner/repo", - }, - ], - [ - "https://github.com/owner/repo/tree/main/plugins/demo", - { - kind: "github", - owner: "owner", - repo: "repo", - ref: "main", - path: "plugins/demo", - url: "https://github.com/owner/repo", - }, - ], - [ - "https://github.com/owner/repo/blob/main/plugins/demo/index.ts", - { - kind: "github", - owner: "owner", - repo: "repo", - ref: "main", - path: "plugins/demo", - url: "https://github.com/owner/repo", - }, - ], - [ - "https://github.com/owner/repo.git", - { - kind: "github", - owner: "owner", - repo: "repo", - path: ".", - url: "https://github.com/owner/repo", - }, - ], - ])("parses %s as a GitHub source", async (input, expected) => { - const workdir = await makeTmpDir(); - const restoreFetch = - input.includes("/tree/") || input.includes("/blob/") - ? mockGitHubCommitLookup([(expected as { ref?: string }).ref ?? ""]) - : null; - try { - await expect(resolveSourceInput(input, { workdir })).resolves.toEqual(expected); - } finally { - restoreFetch?.(); - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("parses tree URLs whose refs contain slashes", async () => { - const workdir = await makeTmpDir(); - const restoreFetch = mockGitHubCommitLookup(["feature/new-ui"]); - try { - await expect( - resolveSourceInput("https://github.com/owner/repo/tree/feature/new-ui/plugins/demo", { - workdir, - }), - ).resolves.toEqual({ - kind: "github", - owner: "owner", - repo: "repo", - ref: "feature/new-ui", - path: "plugins/demo", - url: "https://github.com/owner/repo", - }); - } finally { - restoreFetch(); - await rm(workdir, { recursive: true, force: true }); - } - }); - - it.each([ - "./local-folder", - "/absolute/path", - "~/path", - ".", - "@scope/package", - "owner/repo/extra", - ])("treats %s as a local path", async (input) => { - const workdir = await makeTmpDir(); - try { - const resolved = await resolveSourceInput(input, { workdir }); - expect(resolved.kind).toBe("local"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); +describe('github publish source helpers', () => { + it.each([ + [ + 'owner/repo', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + path: '.', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'owner/repo@v1.0.0', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + ref: 'v1.0.0', + path: '.', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'owner/repo@main', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + ref: 'main', + path: '.', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'https://github.com/owner/repo', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + path: '.', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'https://github.com/owner/repo/tree/main', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + ref: 'main', + path: '.', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'https://github.com/owner/repo/tree/main/plugins/demo', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + ref: 'main', + path: 'plugins/demo', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'https://github.com/owner/repo/blob/main/plugins/demo/index.ts', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + ref: 'main', + path: 'plugins/demo', + url: 'https://github.com/owner/repo', + }, + ], + [ + 'https://github.com/owner/repo.git', + { + kind: 'github', + owner: 'owner', + repo: 'repo', + path: '.', + url: 'https://github.com/owner/repo', + }, + ], + ])('parses %s as a GitHub source', async (input, expected) => { + const workdir = await makeTmpDir(); + const restoreFetch = + input.includes('/tree/') || input.includes('/blob/') + ? mockGitHubCommitLookup([(expected as { ref?: string }).ref ?? '']) + : null; + try { + await expect(resolveSourceInput(input, { workdir })).resolves.toEqual(expected); + } finally { + restoreFetch?.(); + await rm(workdir, { recursive: true, force: true }); + } + }); - it("prefers an existing local directory over GitHub shorthand", async () => { - const workdir = await makeTmpDir(); - try { - const localDir = join(workdir, "owner", "repo"); - await mkdir(localDir, { recursive: true }); + it('parses tree URLs whose refs contain slashes', async () => { + const workdir = await makeTmpDir(); + const restoreFetch = mockGitHubCommitLookup(['feature/new-ui']); + try { + await expect( + resolveSourceInput( + 'https://github.com/owner/repo/tree/feature/new-ui/plugins/demo', + { + workdir, + } + ) + ).resolves.toEqual({ + kind: 'github', + owner: 'owner', + repo: 'repo', + ref: 'feature/new-ui', + path: 'plugins/demo', + url: 'https://github.com/owner/repo', + }); + } finally { + restoreFetch(); + await rm(workdir, { recursive: true, force: true }); + } + }); - await expect(resolveSourceInput("owner/repo", { workdir })).resolves.toEqual({ - kind: "local", - path: localDir, - }); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); + it.each([ + './local-folder', + '/absolute/path', + '~/path', + '.', + '@scope/package', + 'owner/repo/extra', + ])('treats %s as a local path', async (input) => { + const workdir = await makeTmpDir(); + try { + const resolved = await resolveSourceInput(input, { workdir }); + expect(resolved.kind).toBe('local'); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - it("prefers an existing local path from an alternate workdir", async () => { - const workspace = await makeTmpDir(); - const callerCwd = await makeTmpDir(); - try { - const localDir = join(callerCwd, "plugin"); - await mkdir(localDir, { recursive: true }); + it('prefers an existing local directory over GitHub shorthand', async () => { + const workdir = await makeTmpDir(); + try { + const localDir = join(workdir, 'owner', 'repo'); + await mkdir(localDir, { recursive: true }); - await expect( - resolveSourceInput(".", { workdir: workspace, localWorkdirs: [callerCwd, workspace] }), - ).resolves.toEqual({ - kind: "local", - path: callerCwd, - }); + await expect(resolveSourceInput('owner/repo', { workdir })).resolves.toEqual({ + kind: 'local', + path: localDir, + }); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - await expect( - resolveSourceInput("plugin", { workdir: workspace, localWorkdirs: [callerCwd, workspace] }), - ).resolves.toEqual({ - kind: "local", - path: localDir, - }); - } finally { - await rm(callerCwd, { recursive: true, force: true }); - await rm(workspace, { recursive: true, force: true }); - } - }); + it('prefers an existing local path from an alternate workdir', async () => { + const workspace = await makeTmpDir(); + const callerCwd = await makeTmpDir(); + try { + const localDir = join(callerCwd, 'plugin'); + await mkdir(localDir, { recursive: true }); - it("resolves git metadata for a nested folder in a real git repo", async () => { - const root = await makeTmpDir(); - try { - const nested = join(root, "plugins", "demo"); - await mkdir(nested, { recursive: true }); - await writeFile(join(nested, "package.json"), '{"name":"demo"}\n', "utf8"); + await expect( + resolveSourceInput('.', { + workdir: workspace, + localWorkdirs: [callerCwd, workspace], + }) + ).resolves.toEqual({ + kind: 'local', + path: callerCwd, + }); - runGit(root, ["init", "-b", "main"]); - runGit(root, ["remote", "add", "origin", "git@github.com:openclaw/demo-repo.git"]); - runGit(root, ["add", "."]); - runGit(root, [ - "-c", - "user.name=Test", - "-c", - "user.email=test@example.com", - "commit", - "-m", - "init", - ]); - const commit = runGit(root, ["rev-parse", "HEAD"]); - const gitRoot = runGit(root, ["rev-parse", "--show-toplevel"]); - runGit(root, ["-c", "tag.gpgSign=false", "tag", "v1.0.0"]); + await expect( + resolveSourceInput('plugin', { + workdir: workspace, + localWorkdirs: [callerCwd, workspace], + }) + ).resolves.toEqual({ + kind: 'local', + path: localDir, + }); + } finally { + await rm(callerCwd, { recursive: true, force: true }); + await rm(workspace, { recursive: true, force: true }); + } + }); - expect(resolveLocalGitInfo(nested)).toEqual({ - root: gitRoot, - path: "plugins/demo", - repo: "openclaw/demo-repo", - commit, - ref: "v1.0.0", - }); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); + it('resolves git metadata for a nested folder in a real git repo', async () => { + const root = await makeTmpDir(); + try { + const nested = join(root, 'plugins', 'demo'); + await mkdir(nested, { recursive: true }); + await writeFile(join(nested, 'package.json'), '{"name":"demo"}\n', 'utf8'); - it("returns null for a non-git folder", async () => { - const workdir = await makeTmpDir(); - try { - const folder = join(workdir, "not-a-repo"); - await mkdir(folder, { recursive: true }); - expect(resolveLocalGitInfo(folder)).toBeNull(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); + runGit(root, ['init', '-b', 'main']); + runGit(root, ['remote', 'add', 'origin', 'git@github.com:openclaw/demo-repo.git']); + runGit(root, ['add', '.']); + runGit(root, [ + '-c', + 'user.name=Test', + '-c', + 'user.email=test@example.com', + 'commit', + '-m', + 'init', + ]); + const commit = runGit(root, ['rev-parse', 'HEAD']); + const gitRoot = runGit(root, ['rev-parse', '--show-toplevel']); + runGit(root, ['-c', 'tag.gpgSign=false', 'tag', 'v1.0.0']); - it("extracts GitHub archives that contain explicit directory entries", async () => { - const archiveBytes = zipSync({ - "repo-root/.agents/": new Uint8Array(), - "repo-root/.agents/config.json": new TextEncoder().encode('{"ok":true}\n'), - "repo-root/package.json": new TextEncoder().encode('{"name":"demo","version":"1.0.0"}\n'), - "repo-root/openclaw.plugin.json": new TextEncoder().encode( - '{"id":"demo","configSchema":{"type":"object"}}\n', - ), + expect(resolveLocalGitInfo(nested)).toEqual({ + root: gitRoot, + path: 'plugins/demo', + repo: 'openclaw/demo-repo', + commit, + ref: 'v1.0.0', + }); + } finally { + await rm(root, { recursive: true, force: true }); + } }); - const archiveBody = archiveBytes.buffer.slice( - archiveBytes.byteOffset, - archiveBytes.byteOffset + archiveBytes.byteLength, - ) as ArrayBuffer; - const fetchMock = vi - .fn() - .mockResolvedValueOnce( - new Response(JSON.stringify({ default_branch: "main" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ) - .mockResolvedValueOnce( - new Response(JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ) - .mockResolvedValueOnce( - new Response(archiveBody, { - status: 200, - headers: { "content-type": "application/zip" }, - }), - ); - const originalFetch = globalThis.fetch; - Object.defineProperty(globalThis, "fetch", { - value: fetchMock, - configurable: true, - writable: true, + it('returns null for a non-git folder', async () => { + const workdir = await makeTmpDir(); + try { + const folder = join(workdir, 'not-a-repo'); + await mkdir(folder, { recursive: true }); + expect(resolveLocalGitInfo(folder)).toBeNull(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - const fetched = await fetchGitHubSource({ - kind: "github", - owner: "owner", - repo: "repo", - path: ".", - url: "https://github.com/owner/repo", - }); + it('extracts GitHub archives that contain explicit directory entries', async () => { + const archiveBytes = zipSync({ + 'repo-root/.agents/': new Uint8Array(), + 'repo-root/.agents/config.json': new TextEncoder().encode('{"ok":true}\n'), + 'repo-root/package.json': new TextEncoder().encode( + '{"name":"demo","version":"1.0.0"}\n' + ), + 'repo-root/openclaw.plugin.json': new TextEncoder().encode( + '{"id":"demo","configSchema":{"type":"object"}}\n' + ), + }); + const archiveBody = archiveBytes.buffer.slice( + archiveBytes.byteOffset, + archiveBytes.byteOffset + archiveBytes.byteLength + ) as ArrayBuffer; - try { - expect(await readFile(join(fetched.dir, ".agents", "config.json"), "utf8")).toContain( - '"ok":true', - ); - expect(await readFile(join(fetched.dir, "package.json"), "utf8")).toContain('"name":"demo"'); - } finally { - await fetched.cleanup(); - Object.defineProperty(globalThis, "fetch", { - value: originalFetch, - configurable: true, - writable: true, - }); - } - }); + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify({ default_branch: 'main' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ sha: '0123456789abcdef0123456789abcdef01234567' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + ) + .mockResolvedValueOnce( + new Response(archiveBody, { + status: 200, + headers: { 'content-type': 'application/zip' }, + }) + ); + const originalFetch = globalThis.fetch; + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); - it("rejects GitHub archives with unsafe paths", async () => { - const archiveBytes = zipSync({ - "repo-root/../../escape.txt": new TextEncoder().encode("bad\n"), - "repo-root/package.json": new TextEncoder().encode('{"name":"demo","version":"1.0.0"}\n'), - "repo-root/openclaw.plugin.json": new TextEncoder().encode( - '{"id":"demo","configSchema":{"type":"object"}}\n', - ), - }); - const archiveBody = archiveBytes.buffer.slice( - archiveBytes.byteOffset, - archiveBytes.byteOffset + archiveBytes.byteLength, - ) as ArrayBuffer; + const fetched = await fetchGitHubSource({ + kind: 'github', + owner: 'owner', + repo: 'repo', + path: '.', + url: 'https://github.com/owner/repo', + }); - const fetchMock = vi - .fn() - .mockResolvedValueOnce( - new Response(JSON.stringify({ default_branch: "main" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ) - .mockResolvedValueOnce( - new Response(JSON.stringify({ sha: "0123456789abcdef0123456789abcdef01234567" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ) - .mockResolvedValueOnce( - new Response(archiveBody, { - status: 200, - headers: { "content-type": "application/zip" }, - }), - ); - const originalFetch = globalThis.fetch; - Object.defineProperty(globalThis, "fetch", { - value: fetchMock, - configurable: true, - writable: true, + try { + expect(await readFile(join(fetched.dir, '.agents', 'config.json'), 'utf8')).toContain( + '"ok":true' + ); + expect(await readFile(join(fetched.dir, 'package.json'), 'utf8')).toContain( + '"name":"demo"' + ); + } finally { + await fetched.cleanup(); + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } }); - try { - await expect( - fetchGitHubSource({ - kind: "github", - owner: "owner", - repo: "repo", - path: ".", - url: "https://github.com/owner/repo", - }), - ).rejects.toThrow(/Unsafe path in archive/i); - } finally { - Object.defineProperty(globalThis, "fetch", { - value: originalFetch, - configurable: true, - writable: true, - }); - } - }); + it('rejects GitHub archives with unsafe paths', async () => { + const archiveBytes = zipSync({ + 'repo-root/../../escape.txt': new TextEncoder().encode('bad\n'), + 'repo-root/package.json': new TextEncoder().encode( + '{"name":"demo","version":"1.0.0"}\n' + ), + 'repo-root/openclaw.plugin.json': new TextEncoder().encode( + '{"id":"demo","configSchema":{"type":"object"}}\n' + ), + }); + const archiveBody = archiveBytes.buffer.slice( + archiveBytes.byteOffset, + archiveBytes.byteOffset + archiveBytes.byteLength + ) as ArrayBuffer; + + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify({ default_branch: 'main' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify({ sha: '0123456789abcdef0123456789abcdef01234567' }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + ) + .mockResolvedValueOnce( + new Response(archiveBody, { + status: 200, + headers: { 'content-type': 'application/zip' }, + }) + ); + const originalFetch = globalThis.fetch; + Object.defineProperty(globalThis, 'fetch', { + value: fetchMock, + configurable: true, + writable: true, + }); + + try { + await expect( + fetchGitHubSource({ + kind: 'github', + owner: 'owner', + repo: 'repo', + path: '.', + url: 'https://github.com/owner/repo', + }) + ).rejects.toThrow(/Unsafe path in archive/i); + } finally { + Object.defineProperty(globalThis, 'fetch', { + value: originalFetch, + configurable: true, + writable: true, + }); + } + }); }); diff --git a/dt-skill/src/cli/commands/github.ts b/dt-skill/src/cli/commands/github.ts index 13cbbe5c..82a390c0 100644 --- a/dt-skill/src/cli/commands/github.ts +++ b/dt-skill/src/cli/commands/github.ts @@ -1,417 +1,417 @@ -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"; -import { homedir, tmpdir } from "node:os"; -import { dirname, join, resolve, sep } from "node:path"; -import { unzipSync } from "fflate"; +import { spawnSync } from 'node:child_process'; +import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'; +import { homedir, tmpdir } from 'node:os'; +import { dirname, join, resolve, sep } from 'node:path'; +import { unzipSync } from 'fflate'; -const GITHUB_API = "https://api.github.com"; -const GITHUB_HOSTS = new Set(["github.com", "www.github.com"]); -const ZIP_USER_AGENT = "dt-skill/package-publish"; +const GITHUB_API = 'https://api.github.com'; +const GITHUB_HOSTS = new Set(['github.com', 'www.github.com']); +const ZIP_USER_AGENT = 'dt-skill/package-publish'; type ResolvedPublishSource = - | { - kind: "local"; - path: string; - } - | { - kind: "github"; - owner: string; - repo: string; - ref?: string; - path: string; - url: string; - }; + | { + kind: 'local'; + path: string; + } + | { + kind: 'github'; + owner: string; + repo: string; + ref?: string; + path: string; + url: string; + }; type LocalGitInfo = { - root: string; - path: string; - repo?: string; - commit?: string; - ref?: string; + root: string; + path: string; + repo?: string; + commit?: string; + ref?: string; }; type FetchedGitHubSource = { - dir: string; - source: { - kind: "github"; - url: string; - repo: string; - ref: string; - commit: string; - path: string; - importedAt: number; - }; - cleanup: () => Promise; + dir: string; + source: { + kind: 'github'; + url: string; + repo: string; + ref: string; + commit: string; + path: string; + importedAt: number; + }; + cleanup: () => Promise; }; export async function resolveSourceInput( - input: string, - options: { workdir: string; localWorkdirs?: string[] }, + input: string, + options: { workdir: string; localWorkdirs?: string[] } ): Promise { - const trimmed = input.trim(); - if (!trimmed) throw new Error("Path required"); - const localWorkdirs = normalizeLocalWorkdirs(options.workdir, options.localWorkdirs); + const trimmed = input.trim(); + if (!trimmed) throw new Error('Path required'); + const localWorkdirs = normalizeLocalWorkdirs(options.workdir, options.localWorkdirs); - if (trimmed.startsWith("https://")) { - return await parseGitHubUrl(trimmed); - } + if (trimmed.startsWith('https://')) { + return await parseGitHubUrl(trimmed); + } - const shorthand = parseGitHubShorthand(trimmed); - if (shorthand) { - for (const workdir of localWorkdirs) { - const localPath = resolveLocalPath(workdir, trimmed); - const localStat = await stat(localPath).catch(() => null); - if (localStat?.isDirectory()) { - return { kind: "local", path: localPath }; - } + const shorthand = parseGitHubShorthand(trimmed); + if (shorthand) { + for (const workdir of localWorkdirs) { + const localPath = resolveLocalPath(workdir, trimmed); + const localStat = await stat(localPath).catch(() => null); + if (localStat?.isDirectory()) { + return { kind: 'local', path: localPath }; + } + } + return shorthand; } - return shorthand; - } - for (const workdir of localWorkdirs) { - const localPath = resolveLocalPath(workdir, trimmed); - if (await stat(localPath).catch(() => null)) { - return { kind: "local", path: localPath }; + for (const workdir of localWorkdirs) { + const localPath = resolveLocalPath(workdir, trimmed); + if (await stat(localPath).catch(() => null)) { + return { kind: 'local', path: localPath }; + } } - } - return { kind: "local", path: resolveLocalPath(localWorkdirs[0] ?? options.workdir, trimmed) }; + return { kind: 'local', path: resolveLocalPath(localWorkdirs[0] ?? options.workdir, trimmed) }; } export async function fetchGitHubSource( - source: Extract, -) { - const token = process.env.GITHUB_TOKEN?.trim() || undefined; - const repo = `${source.owner}/${source.repo}`; - const repoUrl = `https://github.com/${repo}`; - const resolvedRef = - source.ref?.trim() || (await resolveDefaultBranch(source.owner, source.repo, token)); - const commit = await resolveCommitSha(source.owner, source.repo, resolvedRef, token); - const archiveBytes = await downloadGitHubZip(source.owner, source.repo, commit, token); - const entries = stripSingleTopLevelFolder(unzipSync(archiveBytes)); - const publishPath = normalizeRepoSubpath(source.path); - const tempDir = await mkdtemp(join(tmpdir(), "dt-skill-github-publish-")); - - try { - const subdirEntries = filterEntriesForSubpath(entries, publishPath); - if (Object.keys(subdirEntries).length === 0) { - throw new Error(`GitHub path "${publishPath}" does not contain any files`); + source: Extract +): Promise { + const token = process.env.GITHUB_TOKEN?.trim() || undefined; + const repo = `${source.owner}/${source.repo}`; + const repoUrl = `https://github.com/${repo}`; + const resolvedRef = + source.ref?.trim() || (await resolveDefaultBranch(source.owner, source.repo, token)); + const commit = await resolveCommitSha(source.owner, source.repo, resolvedRef, token); + const archiveBytes = await downloadGitHubZip(source.owner, source.repo, commit, token); + const entries = stripSingleTopLevelFolder(unzipSync(archiveBytes)); + const publishPath = normalizeRepoSubpath(source.path); + const tempDir = await mkdtemp(join(tmpdir(), 'dt-skill-github-publish-')); + + try { + const subdirEntries = filterEntriesForSubpath(entries, publishPath); + if (Object.keys(subdirEntries).length === 0) { + throw new Error(`GitHub path "${publishPath}" does not contain any files`); + } + await writeEntries(tempDir, subdirEntries); + } catch (error) { + await rm(tempDir, { recursive: true, force: true }); + throw error; } - await writeEntries(tempDir, subdirEntries); - } catch (error) { - await rm(tempDir, { recursive: true, force: true }); - throw error; - } - - return { - dir: tempDir, - source: { - kind: "github" as const, - url: repoUrl, - repo, - ref: resolvedRef, - commit, - path: publishPath, - importedAt: Date.now(), - }, - cleanup: async () => { - await rm(tempDir, { recursive: true, force: true }); - }, - } satisfies FetchedGitHubSource; + + return { + dir: tempDir, + source: { + kind: 'github' as const, + url: repoUrl, + repo, + ref: resolvedRef, + commit, + path: publishPath, + importedAt: Date.now(), + }, + cleanup: async () => { + await rm(tempDir, { recursive: true, force: true }); + }, + }; } export function resolveLocalGitInfo(folder: string): LocalGitInfo | null { - const root = runGit(folder, ["rev-parse", "--show-toplevel"]); - if (!root) return null; - - const prefix = runGit(folder, ["rev-parse", "--show-prefix"]); - const commit = runGit(folder, ["rev-parse", "HEAD"]) || undefined; - const ref = - runGit(folder, ["describe", "--tags", "--exact-match"]) || - runGit(folder, ["branch", "--show-current"]) || - commit; - const repo = normalizeGitHubRepo(runGit(folder, ["remote", "get-url", "origin"]) || ""); - - return { - root: root, - path: normalizePath(prefix || "") || ".", - repo: repo || undefined, - commit, - ref: ref || undefined, - }; + const root = runGit(folder, ['rev-parse', '--show-toplevel']); + if (!root) return null; + + const prefix = runGit(folder, ['rev-parse', '--show-prefix']); + const commit = runGit(folder, ['rev-parse', 'HEAD']) || undefined; + const ref = + runGit(folder, ['describe', '--tags', '--exact-match']) || + runGit(folder, ['branch', '--show-current']) || + commit; + const repo = normalizeGitHubRepo(runGit(folder, ['remote', 'get-url', 'origin']) || ''); + + return { + root: root, + path: normalizePath(prefix || '') || '.', + repo: repo || undefined, + commit, + ref: ref || undefined, + }; } export function normalizeGitHubRepo(value: string) { - const trimmed = value - .trim() - .replace(/^git\+/, "") - .replace(/\.git$/i, "") - .replace(/^git@github\.com:/i, "https://github.com/"); - if (!trimmed) return undefined; - - const shorthand = trimmed.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i); - if (shorthand) return `${shorthand[1]}/${shorthand[2]}`; - - try { - const url = new URL(trimmed); - if (!GITHUB_HOSTS.has(url.hostname)) return undefined; - const segments = decodePathSegments(url.pathname); - const owner = segments[0] ?? ""; - const repo = (segments[1] ?? "").replace(/\.git$/i, ""); - if (!owner || !repo) return undefined; - return `${owner}/${repo}`; - } catch { - return undefined; - } + const trimmed = value + .trim() + .replace(/^git\+/, '') + .replace(/\.git$/i, '') + .replace(/^git@github\.com:/i, 'https://github.com/'); + if (!trimmed) return undefined; + + const shorthand = trimmed.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i); + if (shorthand) return `${shorthand[1]}/${shorthand[2]}`; + + try { + const url = new URL(trimmed); + if (!GITHUB_HOSTS.has(url.hostname)) return undefined; + const segments = decodePathSegments(url.pathname); + const owner = segments[0] ?? ''; + const repo = (segments[1] ?? '').replace(/\.git$/i, ''); + if (!owner || !repo) return undefined; + return `${owner}/${repo}`; + } catch { + return undefined; + } } function parseGitHubShorthand( - input: string, -): Extract | null { - const atIndex = input.lastIndexOf("@"); - const rawRepo = atIndex > 0 ? input.slice(0, atIndex) : input; - const rawRef = atIndex > 0 ? input.slice(atIndex + 1).trim() : ""; - if ( - !rawRepo || - rawRepo.startsWith(".") || - rawRepo.startsWith("~") || - rawRepo.startsWith("/") || - rawRepo.includes("\\") - ) { - return null; - } - const match = rawRepo.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i); - if (!match) return null; - - return { - kind: "github", - owner: match[1], - repo: match[2], - ...(rawRef ? { ref: rawRef } : {}), - path: ".", - url: `https://github.com/${match[1]}/${match[2]}`, - }; + input: string +): Extract | null { + const atIndex = input.lastIndexOf('@'); + const rawRepo = atIndex > 0 ? input.slice(0, atIndex) : input; + const rawRef = atIndex > 0 ? input.slice(atIndex + 1).trim() : ''; + if ( + !rawRepo || + rawRepo.startsWith('.') || + rawRepo.startsWith('~') || + rawRepo.startsWith('/') || + rawRepo.includes('\\') + ) { + return null; + } + const match = rawRepo.match(/^([a-z0-9_.-]+)\/([a-z0-9_.-]+)$/i); + if (!match) return null; + + return { + kind: 'github', + owner: match[1], + repo: match[2], + ...(rawRef ? { ref: rawRef } : {}), + path: '.', + url: `https://github.com/${match[1]}/${match[2]}`, + }; } async function parseGitHubUrl( - input: string, -): Promise> { - let url: URL; - try { - url = new URL(input); - } catch { - throw new Error("Invalid GitHub URL"); - } - if (url.protocol !== "https:") throw new Error("Only https:// GitHub URLs are supported"); - if (!GITHUB_HOSTS.has(url.hostname)) throw new Error("Only github.com URLs are supported"); - - const segments = decodePathSegments(url.pathname); - const owner = segments[0] ?? ""; - const repo = (segments[1] ?? "").replace(/\.git$/i, ""); - if (!owner || !repo) throw new Error("GitHub URL must be //"); - - const kind = segments[2] ?? ""; - if (!kind || (kind !== "tree" && kind !== "blob")) { + input: string +): Promise> { + let url: URL; + try { + url = new URL(input); + } catch { + throw new Error('Invalid GitHub URL'); + } + if (url.protocol !== 'https:') throw new Error('Only https:// GitHub URLs are supported'); + if (!GITHUB_HOSTS.has(url.hostname)) throw new Error('Only github.com URLs are supported'); + + const segments = decodePathSegments(url.pathname); + const owner = segments[0] ?? ''; + const repo = (segments[1] ?? '').replace(/\.git$/i, ''); + if (!owner || !repo) throw new Error('GitHub URL must be //'); + + const kind = segments[2] ?? ''; + if (!kind || (kind !== 'tree' && kind !== 'blob')) { + return { + kind: 'github', + owner, + repo, + path: '.', + url: `https://github.com/${owner}/${repo}`, + }; + } + + const { ref, path } = await resolveGitHubUrlRefAndPath(owner, repo, kind, segments.slice(3)); + return { - kind: "github", - owner, - repo, - path: ".", - url: `https://github.com/${owner}/${repo}`, + kind: 'github', + owner, + repo, + ref, + path, + url: `https://github.com/${owner}/${repo}`, }; - } - - const { ref, path } = await resolveGitHubUrlRefAndPath(owner, repo, kind, segments.slice(3)); - - return { - kind: "github", - owner, - repo, - ref, - path, - url: `https://github.com/${owner}/${repo}`, - }; } function normalizeRepoSubpath(value: string) { - const normalized = normalizePath(value.trim()); - if (!normalized || normalized === ".") return "."; - const segments = normalized.split("/"); - if (segments.some((segment) => !segment || segment === "." || segment === "..")) { - throw new Error("Invalid GitHub path"); - } - return segments.join("/"); + const normalized = normalizePath(value.trim()); + if (!normalized || normalized === '.') return '.'; + const segments = normalized.split('/'); + if (segments.some((segment) => !segment || segment === '.' || segment === '..')) { + throw new Error('Invalid GitHub path'); + } + return segments.join('/'); } function resolveLocalPath(workdir: string, input: string) { - if (input === "~") return homedir(); - if (input.startsWith("~/")) return resolve(homedir(), input.slice(2)); - return resolve(workdir, input); + if (input === '~') return homedir(); + if (input.startsWith('~/')) return resolve(homedir(), input.slice(2)); + return resolve(workdir, input); } function normalizeLocalWorkdirs(workdir: string, localWorkdirs?: string[]) { - const values = localWorkdirs?.length ? localWorkdirs : [workdir]; - return Array.from(new Set(values.map((value) => resolve(value)))); + const values = localWorkdirs?.length ? localWorkdirs : [workdir]; + return Array.from(new Set(values.map((value) => resolve(value)))); } function normalizePath(pathValue: string) { - return pathValue - .split(/[\\/]+/) - .filter(Boolean) - .join("/") - .replace(/^\.\/+/, ""); + return pathValue + .split(/[\\/]+/) + .filter(Boolean) + .join('/') + .replace(/^\.\/+/, ''); } function decodePathSegments(pathname: string) { - return pathname - .split("/") - .map((segment) => segment.trim()) - .filter(Boolean) - .map((segment) => { - try { - return decodeURIComponent(segment); - } catch { - throw new Error("Invalid GitHub URL"); - } - }); + return pathname + .split('/') + .map((segment) => segment.trim()) + .filter(Boolean) + .map((segment) => { + try { + return decodeURIComponent(segment); + } catch { + throw new Error('Invalid GitHub URL'); + } + }); } async function resolveDefaultBranch(owner: string, repo: string, token?: string) { - const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}`, { - headers: buildGitHubHeaders(token), - }); - if (!response.ok) throw new Error(`GitHub repo not found: ${owner}/${repo}`); - const parsed = (await response.json()) as { default_branch?: unknown }; - const defaultBranch = - typeof parsed.default_branch === "string" ? parsed.default_branch.trim() : ""; - if (!defaultBranch) throw new Error("GitHub repo default branch missing"); - return defaultBranch; + const response = await fetch(`${GITHUB_API}/repos/${owner}/${repo}`, { + headers: buildGitHubHeaders(token), + }); + if (!response.ok) throw new Error(`GitHub repo not found: ${owner}/${repo}`); + const parsed = (await response.json()) as { default_branch?: unknown }; + const defaultBranch = + typeof parsed.default_branch === 'string' ? parsed.default_branch.trim() : ''; + if (!defaultBranch) throw new Error('GitHub repo default branch missing'); + return defaultBranch; } async function resolveCommitSha(owner: string, repo: string, ref: string, token?: string) { - const response = await fetch( - `${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, - { - headers: buildGitHubHeaders(token), - }, - ); - if (!response.ok) throw new Error(`GitHub ref not found: ${owner}/${repo}@${ref}`); - const parsed = (await response.json()) as { sha?: unknown }; - const sha = typeof parsed.sha === "string" ? parsed.sha.trim().toLowerCase() : ""; - if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error("GitHub commit sha missing"); - return sha; + const response = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, + { + headers: buildGitHubHeaders(token), + } + ); + if (!response.ok) throw new Error(`GitHub ref not found: ${owner}/${repo}@${ref}`); + const parsed = (await response.json()) as { sha?: unknown }; + const sha = typeof parsed.sha === 'string' ? parsed.sha.trim().toLowerCase() : ''; + if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error('GitHub commit sha missing'); + return sha; } async function tryResolveCommitSha(owner: string, repo: string, ref: string, token?: string) { - const response = await fetch( - `${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, - { - headers: buildGitHubHeaders(token), - }, - ); - if (!response.ok) return null; - const parsed = (await response.json()) as { sha?: unknown }; - const sha = typeof parsed.sha === "string" ? parsed.sha.trim().toLowerCase() : ""; - return /^[a-f0-9]{40}$/.test(sha) ? sha : null; + const response = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, + { + headers: buildGitHubHeaders(token), + } + ); + if (!response.ok) return null; + const parsed = (await response.json()) as { sha?: unknown }; + const sha = typeof parsed.sha === 'string' ? parsed.sha.trim().toLowerCase() : ''; + return /^[a-f0-9]{40}$/.test(sha) ? sha : null; } async function downloadGitHubZip(owner: string, repo: string, ref: string, token?: string) { - const response = await fetch( - `${GITHUB_API}/repos/${owner}/${repo}/zipball/${encodeURIComponent(ref)}`, - { - headers: buildGitHubHeaders(token), - }, - ); - if (!response.ok) throw new Error(`GitHub archive download failed: ${owner}/${repo}@${ref}`); - return new Uint8Array(await response.arrayBuffer()); + const response = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/zipball/${encodeURIComponent(ref)}`, + { + headers: buildGitHubHeaders(token), + } + ); + if (!response.ok) throw new Error(`GitHub archive download failed: ${owner}/${repo}@${ref}`); + return new Uint8Array(await response.arrayBuffer()); } function buildGitHubHeaders(token?: string) { - const headers: Record = { - Accept: "application/vnd.github+json", - "User-Agent": ZIP_USER_AGENT, - }; - if (token) headers.Authorization = `Bearer ${token}`; - return headers; + const headers: Record = { + Accept: 'application/vnd.github+json', + 'User-Agent': ZIP_USER_AGENT, + }; + if (token) headers.Authorization = `Bearer ${token}`; + return headers; } function stripSingleTopLevelFolder(entries: Record) { - const paths = Object.keys(entries); - if (paths.length === 0) return {}; - const firstRoot = paths[0]?.split("/")[0] ?? ""; - if (!firstRoot) return entries; - const prefix = `${firstRoot}/`; - if (!paths.every((path) => path.startsWith(prefix))) return entries; - - const stripped: Record = {}; - for (const [path, bytes] of Object.entries(entries)) { - const next = path.slice(prefix.length); - if (!next) continue; - stripped[next] = bytes; - } - return stripped; + const paths = Object.keys(entries); + if (paths.length === 0) return {}; + const firstRoot = paths[0]?.split('/')[0] ?? ''; + if (!firstRoot) return entries; + const prefix = `${firstRoot}/`; + if (!paths.every((path) => path.startsWith(prefix))) return entries; + + const stripped: Record = {}; + for (const [path, bytes] of Object.entries(entries)) { + const next = path.slice(prefix.length); + if (!next) continue; + stripped[next] = bytes; + } + return stripped; } function filterEntriesForSubpath(entries: Record, subpath: string) { - if (subpath === ".") return entries; - const prefix = `${subpath}/`; - const filtered: Record = {}; - for (const [path, bytes] of Object.entries(entries)) { - if (!path.startsWith(prefix)) continue; - const relPath = path.slice(prefix.length); - if (!relPath) continue; - filtered[relPath] = bytes; - } - return filtered; + if (subpath === '.') return entries; + const prefix = `${subpath}/`; + const filtered: Record = {}; + for (const [path, bytes] of Object.entries(entries)) { + if (!path.startsWith(prefix)) continue; + const relPath = path.slice(prefix.length); + if (!relPath) continue; + filtered[relPath] = bytes; + } + return filtered; } async function writeEntries(root: string, entries: Record) { - const absRoot = resolve(root); - for (const [path, bytes] of Object.entries(entries)) { - if (!path || path.endsWith("/")) continue; - const absPath = resolve(absRoot, ...path.split("/")); - if (absPath !== absRoot && !absPath.startsWith(`${absRoot}${sep}`)) { - throw new Error(`Unsafe path in archive: ${path}`); + const absRoot = resolve(root); + for (const [path, bytes] of Object.entries(entries)) { + if (!path || path.endsWith('/')) continue; + const absPath = resolve(absRoot, ...path.split('/')); + if (absPath !== absRoot && !absPath.startsWith(`${absRoot}${sep}`)) { + throw new Error(`Unsafe path in archive: ${path}`); + } + await mkdir(dirname(absPath), { recursive: true }); + await writeFile(absPath, Buffer.from(bytes)); } - await mkdir(dirname(absPath), { recursive: true }); - await writeFile(absPath, Buffer.from(bytes)); - } } async function resolveGitHubUrlRefAndPath( - owner: string, - repo: string, - kind: "tree" | "blob", - segments: string[], + owner: string, + repo: string, + kind: 'tree' | 'blob', + segments: string[] ) { - if (segments.length === 0) throw new Error("Missing ref in GitHub URL"); - - const token = process.env.GITHUB_TOKEN?.trim() || undefined; - const minPathSegments = kind === "blob" ? 1 : 0; - const maxRefSegments = segments.length - minPathSegments; - - for (let refSegmentCount = maxRefSegments; refSegmentCount >= 1; refSegmentCount -= 1) { - const ref = segments.slice(0, refSegmentCount).join("/"); - const pathRemainder = segments.slice(refSegmentCount).join("/"); - if (kind === "blob" && !pathRemainder) continue; - const commit = await tryResolveCommitSha(owner, repo, ref, token); - if (!commit) continue; - const path = - kind === "blob" - ? normalizeRepoSubpath(pathRemainder.split("/").slice(0, -1).join("/") || ".") - : normalizeRepoSubpath(pathRemainder || "."); - return { ref, path }; - } - - throw new Error("GitHub ref not found in URL"); + if (segments.length === 0) throw new Error('Missing ref in GitHub URL'); + + const token = process.env.GITHUB_TOKEN?.trim() || undefined; + const minPathSegments = kind === 'blob' ? 1 : 0; + const maxRefSegments = segments.length - minPathSegments; + + for (let refSegmentCount = maxRefSegments; refSegmentCount >= 1; refSegmentCount -= 1) { + const ref = segments.slice(0, refSegmentCount).join('/'); + const pathRemainder = segments.slice(refSegmentCount).join('/'); + if (kind === 'blob' && !pathRemainder) continue; + const commit = await tryResolveCommitSha(owner, repo, ref, token); + if (!commit) continue; + const path = + kind === 'blob' + ? normalizeRepoSubpath(pathRemainder.split('/').slice(0, -1).join('/') || '.') + : normalizeRepoSubpath(pathRemainder || '.'); + return { ref, path }; + } + + throw new Error('GitHub ref not found in URL'); } function runGit(cwd: string, args: string[]) { - const result = spawnSync("git", ["-C", cwd, ...args], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - if (result.status !== 0) return null; - const value = result.stdout.trim(); - return value || null; + const result = spawnSync('git', ['-C', cwd, ...args], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (result.status !== 0) return null; + const value = result.stdout.trim(); + return value || null; } diff --git a/dt-skill/src/cli/commands/inspect.test.ts b/dt-skill/src/cli/commands/inspect.test.ts index f5a7a946..45723980 100644 --- a/dt-skill/src/cli/commands/inspect.test.ts +++ b/dt-skill/src/cli/commands/inspect.test.ts @@ -1,221 +1,238 @@ /* @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { - createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; -import { ApiRoutes } from "../../schema/index.js"; + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from '../../../test/cliCommandTestKit.js'; +import { ApiRoutes } from '../../schema/index.js'; const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); const uiMocks = createUiModuleMocks(); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../ui.js", () => uiMocks.moduleFactory()); +vi.mock('../../http.js', () => httpMocks.moduleFactory()); +vi.mock('../registry.js', () => registryMocks.moduleFactory()); +vi.mock('../ui.js', () => uiMocks.moduleFactory()); -const { cmdInspect } = await import("./inspect"); +const { cmdInspect } = await import('./inspect'); -const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); -const mockWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true); +const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {}); +const mockWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); afterEach(() => { - vi.clearAllMocks(); - mockLog.mockClear(); - mockWrite.mockClear(); + vi.clearAllMocks(); + mockLog.mockClear(); + mockWrite.mockClear(); }); -describe("cmdInspect", () => { - it("fetches latest version files when --files is set", async () => { - httpMocks.apiRequest - .mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: { latest: "1.2.3" }, - stats: {}, - createdAt: 1, - updatedAt: 2, - }, - latestVersion: { version: "1.2.3", createdAt: 3, changelog: "init", license: "MIT-0" }, - owner: null, - }) - .mockResolvedValueOnce({ - skill: { slug: "demo", displayName: "Demo" }, - version: { version: "1.2.3", createdAt: 3, changelog: "init", files: [] }, - }); - - await cmdInspect(makeGlobalOpts(), "demo", { files: true }); - - const firstArgs = httpMocks.apiRequest.mock.calls[0]?.[1]; - const secondArgs = httpMocks.apiRequest.mock.calls[1]?.[1]; - expect(firstArgs?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent("demo")}`); - expect(secondArgs?.path).toBe( - `${ApiRoutes.skills}/${encodeURIComponent("demo")}/versions/${encodeURIComponent("1.2.3")}`, - ); - }); - - it("uses tag param when fetching a file", async () => { - httpMocks.apiRequest - .mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: { latest: "2.0.0" }, - stats: {}, - createdAt: 1, - updatedAt: 2, - }, - latestVersion: { version: "2.0.0", createdAt: 3, changelog: "init", license: "MIT-0" }, - owner: null, - }) - .mockResolvedValueOnce({ - skill: { slug: "demo", displayName: "Demo" }, - version: { version: "2.0.0", createdAt: 3, changelog: "init", files: [] }, - }); - httpMocks.fetchText.mockResolvedValue("content"); - - await cmdInspect(makeGlobalOpts(), "demo", { file: "SKILL.md", tag: "latest" }); - - const fetchArgs = httpMocks.fetchText.mock.calls[0]?.[1]; - const url = new URL(String(fetchArgs?.url)); - expect(url.pathname).toBe("/api/v1/skills/demo/file"); - expect(url.searchParams.get("path")).toBe("SKILL.md"); - expect(url.searchParams.get("tag")).toBe("latest"); - expect(url.searchParams.get("version")).toBeNull(); - }); - - it("prints security summary when version security metadata exists", async () => { - httpMocks.apiRequest - .mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: { latest: "2.0.0" }, - stats: {}, - createdAt: 1, - updatedAt: 2, - }, - latestVersion: { version: "2.0.0", createdAt: 3, changelog: "init", license: "MIT-0" }, - owner: null, - }) - .mockResolvedValueOnce({ - skill: { slug: "demo", displayName: "Demo" }, - version: { - version: "2.0.0", - createdAt: 3, - changelog: "init", - files: [], - security: { - status: "suspicious", - hasWarnings: true, - checkedAt: 1_700_000_000_000, - model: "gpt-5.2", - }, - }, - }); - - await cmdInspect(makeGlobalOpts(), "demo", { version: "2.0.0" }); - - expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("License: MIT-0")); - expect(mockLog).toHaveBeenCalledWith("Security: SUSPICIOUS"); - expect(mockLog).toHaveBeenCalledWith("Warnings: yes"); - expect(mockLog).toHaveBeenCalledWith("Checked: 2023-11-14T22:13:20.000Z"); - expect(mockLog).toHaveBeenCalledWith("Model: gpt-5.2"); - }); - - it("prints skill moderation status without requiring a version fetch", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: { latest: "2.0.0" }, - stats: {}, - createdAt: 1, - updatedAt: 2, - }, - latestVersion: { version: "2.0.0", createdAt: 3, changelog: "init", license: "MIT-0" }, - owner: null, - moderation: { - isSuspicious: true, - isMalwareBlocked: false, - verdict: "suspicious", - reasonCodes: ["network-send", "credential-pattern"], - updatedAt: 1_700_000_000_000, - engineVersion: "scanner-v2", - summary: "Found credential-like configuration and outbound network behavior.", - }, +describe('cmdInspect', () => { + it('fetches latest version files when --files is set', async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: { latest: '1.2.3' }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { + version: '1.2.3', + createdAt: 3, + changelog: 'init', + license: 'MIT-0', + }, + owner: null, + }) + .mockResolvedValueOnce({ + skill: { slug: 'demo', displayName: 'Demo' }, + version: { version: '1.2.3', createdAt: 3, changelog: 'init', files: [] }, + }); + + await cmdInspect(makeGlobalOpts(), 'demo', { files: true }); + + const firstArgs = httpMocks.apiRequest.mock.calls[0]?.[1]; + const secondArgs = httpMocks.apiRequest.mock.calls[1]?.[1]; + expect(firstArgs?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent('demo')}`); + expect(secondArgs?.path).toBe( + `${ApiRoutes.skills}/${encodeURIComponent('demo')}/versions/${encodeURIComponent( + '1.2.3' + )}` + ); }); - await cmdInspect(makeGlobalOpts(), "demo"); - - expect(httpMocks.apiRequest).toHaveBeenCalledTimes(1); - expect(mockLog).toHaveBeenCalledWith("Moderation: SUSPICIOUS"); - expect(mockLog).toHaveBeenCalledWith("Reasons: network-send, credential-pattern"); - expect(mockLog).toHaveBeenCalledWith("Moderation Updated: 2023-11-14T22:13:20.000Z"); - expect(mockLog).toHaveBeenCalledWith("Moderation Engine: scanner-v2"); - expect(mockLog).toHaveBeenCalledWith( - "Moderation Summary: Found credential-like configuration and outbound network behavior.", - ); - }); - - it("does not fall back to an authenticated moderation endpoint", async () => { - httpMocks.apiRequest.mockRejectedValueOnce(new Error("Skill is hidden by quality checks.")); - - await expect(cmdInspect(makeGlobalOpts(), "demo")).rejects.toThrow( - "Skill is hidden by quality checks.", - ); - - expect(httpMocks.apiRequest).toHaveBeenCalledTimes(1); - }); - - it("includes moderation metadata in inspect JSON output", async () => { - httpMocks.apiRequest.mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: {}, - stats: {}, - createdAt: 1, - updatedAt: 2, - }, - latestVersion: null, - owner: null, - moderation: { - isSuspicious: false, - isMalwareBlocked: false, - verdict: "clean", - reasonCodes: [], - updatedAt: null, - engineVersion: null, - summary: null, - }, + it('uses tag param when fetching a file', async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: { latest: '2.0.0' }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { + version: '2.0.0', + createdAt: 3, + changelog: 'init', + license: 'MIT-0', + }, + owner: null, + }) + .mockResolvedValueOnce({ + skill: { slug: 'demo', displayName: 'Demo' }, + version: { version: '2.0.0', createdAt: 3, changelog: 'init', files: [] }, + }); + httpMocks.fetchText.mockResolvedValue('content'); + + await cmdInspect(makeGlobalOpts(), 'demo', { file: 'SKILL.md', tag: 'latest' }); + + const fetchArgs = httpMocks.fetchText.mock.calls[0]?.[1]; + const url = new URL(String(fetchArgs?.url)); + expect(url.pathname).toBe('/api/v1/skills/demo/file'); + expect(url.searchParams.get('path')).toBe('SKILL.md'); + expect(url.searchParams.get('tag')).toBe('latest'); + expect(url.searchParams.get('version')).toBeNull(); }); - await cmdInspect(makeGlobalOpts(), "demo", { json: true }); - - const output = JSON.parse(String(mockLog.mock.calls[0]?.[0])); - expect(output.moderation).toEqual({ - isSuspicious: false, - isMalwareBlocked: false, - verdict: "clean", - reasonCodes: [], - updatedAt: null, - engineVersion: null, - summary: null, + it('prints security summary when version security metadata exists', async () => { + httpMocks.apiRequest + .mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: { latest: '2.0.0' }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { + version: '2.0.0', + createdAt: 3, + changelog: 'init', + license: 'MIT-0', + }, + owner: null, + }) + .mockResolvedValueOnce({ + skill: { slug: 'demo', displayName: 'Demo' }, + version: { + version: '2.0.0', + createdAt: 3, + changelog: 'init', + files: [], + security: { + status: 'suspicious', + hasWarnings: true, + checkedAt: 1_700_000_000_000, + model: 'gpt-5.2', + }, + }, + }); + + await cmdInspect(makeGlobalOpts(), 'demo', { version: '2.0.0' }); + + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining('License: MIT-0')); + expect(mockLog).toHaveBeenCalledWith('Security: SUSPICIOUS'); + expect(mockLog).toHaveBeenCalledWith('Warnings: yes'); + expect(mockLog).toHaveBeenCalledWith('Checked: 2023-11-14T22:13:20.000Z'); + expect(mockLog).toHaveBeenCalledWith('Model: gpt-5.2'); }); - }); - it("rejects when both version and tag are provided", async () => { - await expect( - cmdInspect(makeGlobalOpts(), "demo", { version: "1.0.0", tag: "latest" }), - ).rejects.toThrow("Use either --version or --tag"); - }); + it('prints skill moderation status without requiring a version fetch', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: { latest: '2.0.0' }, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: { version: '2.0.0', createdAt: 3, changelog: 'init', license: 'MIT-0' }, + owner: null, + moderation: { + isSuspicious: true, + isMalwareBlocked: false, + verdict: 'suspicious', + reasonCodes: ['network-send', 'credential-pattern'], + updatedAt: 1_700_000_000_000, + engineVersion: 'scanner-v2', + summary: 'Found credential-like configuration and outbound network behavior.', + }, + }); + + await cmdInspect(makeGlobalOpts(), 'demo'); + + expect(httpMocks.apiRequest).toHaveBeenCalledTimes(1); + expect(mockLog).toHaveBeenCalledWith('Moderation: SUSPICIOUS'); + expect(mockLog).toHaveBeenCalledWith('Reasons: network-send, credential-pattern'); + expect(mockLog).toHaveBeenCalledWith('Moderation Updated: 2023-11-14T22:13:20.000Z'); + expect(mockLog).toHaveBeenCalledWith('Moderation Engine: scanner-v2'); + expect(mockLog).toHaveBeenCalledWith( + 'Moderation Summary: Found credential-like configuration and outbound network behavior.' + ); + }); + + it('does not fall back to an authenticated moderation endpoint', async () => { + httpMocks.apiRequest.mockRejectedValueOnce(new Error('Skill is hidden by quality checks.')); + + await expect(cmdInspect(makeGlobalOpts(), 'demo')).rejects.toThrow( + 'Skill is hidden by quality checks.' + ); + + expect(httpMocks.apiRequest).toHaveBeenCalledTimes(1); + }); + + it('includes moderation metadata in inspect JSON output', async () => { + httpMocks.apiRequest.mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: {}, + stats: {}, + createdAt: 1, + updatedAt: 2, + }, + latestVersion: null, + owner: null, + moderation: { + isSuspicious: false, + isMalwareBlocked: false, + verdict: 'clean', + reasonCodes: [], + updatedAt: null, + engineVersion: null, + summary: null, + }, + }); + + await cmdInspect(makeGlobalOpts(), 'demo', { json: true }); + + const output = JSON.parse(String(mockLog.mock.calls[0]?.[0])); + expect(output.moderation).toEqual({ + isSuspicious: false, + isMalwareBlocked: false, + verdict: 'clean', + reasonCodes: [], + updatedAt: null, + engineVersion: null, + summary: null, + }); + }); + + it('rejects when both version and tag are provided', async () => { + await expect( + cmdInspect(makeGlobalOpts(), 'demo', { version: '1.0.0', tag: 'latest' }) + ).rejects.toThrow('Use either --version or --tag'); + }); }); diff --git a/dt-skill/src/cli/commands/inspect.ts b/dt-skill/src/cli/commands/inspect.ts index 4274c87a..3dfdc95d 100644 --- a/dt-skill/src/cli/commands/inspect.ts +++ b/dt-skill/src/cli/commands/inspect.ts @@ -1,445 +1,452 @@ -import { apiRequest, fetchText, registryUrl } from "../../http.js"; +import { apiRequest, fetchText, registryUrl } from '../../http.js'; import { - ApiRoutes, - PLATFORM_SKILL_LICENSE, - PLATFORM_SKILL_LICENSE_SUMMARY, - ApiV1SkillResponseSchema, - ApiV1SkillVersionListResponseSchema, - ApiV1SkillVersionResponseSchema, -} from "../../schema/index.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError } from "../ui.js"; + ApiRoutes, + PLATFORM_SKILL_LICENSE, + PLATFORM_SKILL_LICENSE_SUMMARY, + ApiV1SkillResponseSchema, + ApiV1SkillVersionListResponseSchema, + ApiV1SkillVersionResponseSchema, +} from '../../schema/index.js'; +import { getRegistry } from '../registry.js'; +import type { GlobalOpts } from '../types.js'; +import { createSpinner, fail, formatError } from '../ui.js'; type InspectOptions = { - version?: string; - tag?: string; - versions?: boolean; - limit?: number; - files?: boolean; - file?: string; - json?: boolean; + version?: string; + tag?: string; + versions?: boolean; + limit?: number; + files?: boolean; + file?: string; + json?: boolean; }; type FileEntry = { - path: string; - size: number | null; - sha256: string | null; - contentType: string | null; + path: string; + size: number | null; + sha256: string | null; + contentType: string | null; }; type SecurityStatus = { - status: "clean" | "suspicious" | "malicious" | "pending" | "error"; - hasWarnings: boolean; - checkedAt: number | null; - model: string | null; + status: 'clean' | 'suspicious' | 'malicious' | 'pending' | 'error'; + hasWarnings: boolean; + checkedAt: number | null; + model: string | null; }; type ModerationStatus = { - isSuspicious: boolean; - isMalwareBlocked: boolean; - verdict?: "clean" | "suspicious" | "malicious"; - reasonCodes?: string[]; - updatedAt?: number | null; - engineVersion?: string | null; - summary?: string | null; - legacyReason?: string | null; + isSuspicious: boolean; + isMalwareBlocked: boolean; + verdict?: 'clean' | 'suspicious' | 'malicious'; + reasonCodes?: string[]; + updatedAt?: number | null; + engineVersion?: string | null; + summary?: string | null; + legacyReason?: string | null; }; export async function cmdInspect(opts: GlobalOpts, slug: string, options: InspectOptions = {}) { - const trimmed = slug.trim(); - if (!trimmed) fail("Slug required"); - if (options.version && options.tag) fail("Use either --version or --tag"); - - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Fetching skill"); - try { - let skillResult: Awaited> | null = null; - try { - skillResult = await fetchSkillDetail(registry, trimmed); - } catch (error) { - throw error; - } - - if (!skillResult.skill) { - spinner.fail("Skill not found"); - return; - } + const trimmed = slug.trim(); + if (!trimmed) fail('Slug required'); + if (options.version && options.tag) fail('Use either --version or --tag'); - const skill = skillResult.skill; - const tags = normalizeTags(skill.tags); - const latestVersion = skillResult.latestVersion?.version ?? tags.latest ?? null; - const taggedVersion = options.tag ? (tags[options.tag] ?? null) : null; - if (options.tag && !taggedVersion) { - spinner.fail(`Unknown tag "${options.tag}"`); - return; - } - const requestedVersion = options.version ?? taggedVersion ?? null; - - let versionResult: { version: unknown; skill: unknown } | null = null; - if (options.files || options.file || options.version || options.tag) { - const targetVersion = requestedVersion ?? latestVersion; - if (!targetVersion) fail("Could not resolve latest version"); - spinner.text = `Fetching ${trimmed}@${targetVersion}`; - versionResult = await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions/${encodeURIComponent( - targetVersion, - )}`, - }, - ApiV1SkillVersionResponseSchema, - ); - } + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner('Fetching skill'); + try { + let skillResult: Awaited> | null = null; + try { + skillResult = await fetchSkillDetail(registry, trimmed); + } catch (error) { + throw error; + } - let versionsList: { items?: unknown[]; nextCursor?: string | null } | null = null; - if (options.versions) { - const limit = clampLimit(options.limit ?? 25, 25); - const url = registryUrl( - `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions`, - registry, - ); - url.searchParams.set("limit", String(limit)); - spinner.text = `Fetching versions (${limit})`; - versionsList = await apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1SkillVersionListResponseSchema, - ); - } + if (!skillResult.skill) { + spinner.fail('Skill not found'); + return; + } - let fileContent: string | null = null; - if (options.file) { - const url = registryUrl(`${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/file`, registry); - url.searchParams.set("path", options.file); - if (options.version) { - url.searchParams.set("version", options.version); - } else if (options.tag) { - url.searchParams.set("tag", options.tag); - } else if (latestVersion) { - url.searchParams.set("version", latestVersion); - } - spinner.text = `Fetching ${options.file}`; - fileContent = await fetchText(registry, { url: url.toString() }); - } + const skill = skillResult.skill; + const tags = normalizeTags(skill.tags); + const latestVersion = skillResult.latestVersion?.version ?? tags.latest ?? null; + const taggedVersion = options.tag ? tags[options.tag] ?? null : null; + if (options.tag && !taggedVersion) { + spinner.fail(`Unknown tag "${options.tag}"`); + return; + } + const requestedVersion = options.version ?? taggedVersion ?? null; + + let versionResult: { version: unknown; skill: unknown } | null = null; + if (options.files || options.file || options.version || options.tag) { + const targetVersion = requestedVersion ?? latestVersion; + if (!targetVersion) fail('Could not resolve latest version'); + spinner.text = `Fetching ${trimmed}@${targetVersion}`; + versionResult = await apiRequest( + registry, + { + method: 'GET', + path: `${ApiRoutes.skills}/${encodeURIComponent( + trimmed + )}/versions/${encodeURIComponent(targetVersion)}`, + }, + ApiV1SkillVersionResponseSchema + ); + } - spinner.stop(); + let versionsList: { items?: unknown[]; nextCursor?: string | null } | null = null; + if (options.versions) { + const limit = clampLimit(options.limit ?? 25, 25); + const url = registryUrl( + `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions`, + registry + ); + url.searchParams.set('limit', String(limit)); + spinner.text = `Fetching versions (${limit})`; + versionsList = await apiRequest( + registry, + { method: 'GET', url: url.toString() }, + ApiV1SkillVersionListResponseSchema + ); + } - const output = { - skill: skillResult.skill, - latestVersion: skillResult.latestVersion, - owner: skillResult.owner, - moderation: skillResult.moderation ?? null, - version: versionResult?.version ?? null, - versions: versionsList?.items ?? null, - file: options.file ? { path: options.file, content: fileContent } : null, - }; + let fileContent: string | null = null; + if (options.file) { + const url = registryUrl( + `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/file`, + registry + ); + url.searchParams.set('path', options.file); + if (options.version) { + url.searchParams.set('version', options.version); + } else if (options.tag) { + url.searchParams.set('tag', options.tag); + } else if (latestVersion) { + url.searchParams.set('version', latestVersion); + } + spinner.text = `Fetching ${options.file}`; + fileContent = await fetchText(registry, { url: url.toString() }); + } - if (options.json) { - console.log(JSON.stringify(output, null, 2)); - return; - } + spinner.stop(); + + const output = { + skill: skillResult.skill, + latestVersion: skillResult.latestVersion, + owner: skillResult.owner, + moderation: skillResult.moderation ?? null, + version: versionResult?.version ?? null, + versions: versionsList?.items ?? null, + file: options.file ? { path: options.file, content: fileContent } : null, + }; + + if (options.json) { + console.log(JSON.stringify(output, null, 2)); + return; + } - const shouldPrintMeta = !options.file || options.files || options.versions || options.version; - if (shouldPrintMeta) { - printSkillSummary({ - skill, - latestVersion: skillResult.latestVersion, - versionLicense: - (versionResult?.version as { license?: string | null } | undefined)?.license ?? null, - owner: skillResult.owner, - }); - printModerationSummary(skillResult.moderation ?? null); - } + const shouldPrintMeta = + !options.file || options.files || options.versions || options.version; + if (shouldPrintMeta) { + printSkillSummary({ + skill, + latestVersion: skillResult.latestVersion, + versionLicense: + (versionResult?.version as { license?: string | null } | undefined)?.license ?? + null, + owner: skillResult.owner, + }); + printModerationSummary(skillResult.moderation ?? null); + } - if (shouldPrintMeta && versionResult?.version) { - printVersionSummary(versionResult.version); - printSecuritySummary(versionResult.version); - } + if (shouldPrintMeta && versionResult?.version) { + printVersionSummary(versionResult.version); + printSecuritySummary(versionResult.version); + } - if (versionsList?.items && Array.isArray(versionsList.items)) { - if (versionsList.items.length === 0) { - console.log("No versions found."); - } else { - console.log("Versions:"); - for (const item of versionsList.items) { - console.log(formatVersionLine(item)); + if (versionsList?.items && Array.isArray(versionsList.items)) { + if (versionsList.items.length === 0) { + console.log('No versions found.'); + } else { + console.log('Versions:'); + for (const item of versionsList.items) { + console.log(formatVersionLine(item)); + } + } } - } - } - if (versionResult?.version) { - const files = normalizeFiles((versionResult.version as { files?: unknown }).files); - if (options.files) { - if (files.length === 0) { - console.log("No files found."); - } else { - console.log("Files:"); - for (const file of files) { - console.log(formatFileLine(file)); - } + if (versionResult?.version) { + const files = normalizeFiles((versionResult.version as { files?: unknown }).files); + if (options.files) { + if (files.length === 0) { + console.log('No files found.'); + } else { + console.log('Files:'); + for (const file of files) { + console.log(formatFileLine(file)); + } + } + } } - } - } - if (options.file && fileContent !== null) { - if (shouldPrintMeta) console.log(`\n${options.file}:\n`); - process.stdout.write(fileContent); - if (!fileContent.endsWith("\n")) process.stdout.write("\n"); + if (options.file && fileContent !== null) { + if (shouldPrintMeta) console.log(`\n${options.file}:\n`); + process.stdout.write(fileContent); + if (!fileContent.endsWith('\n')) process.stdout.write('\n'); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; } - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } } function fetchSkillDetail(registry: string, slug: string) { - return apiRequest( - registry, - { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}` }, - ApiV1SkillResponseSchema, - ); + return apiRequest( + registry, + { method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(slug)}` }, + ApiV1SkillResponseSchema + ); } function printSkillSummary(result: { - skill: { - slug: string; - displayName: string; - summary?: string | null; - tags?: unknown; - stats?: unknown; - createdAt: number; - updatedAt: number; - }; - latestVersion?: { - version: string; - createdAt: number; - changelog: string; - license?: string | null; - } | null; - versionLicense?: string | null; - owner?: { handle?: string | null; displayName?: string | null; image?: string | null } | null; + skill: { + slug: string; + displayName: string; + summary?: string | null; + tags?: unknown; + stats?: unknown; + createdAt: number; + updatedAt: number; + }; + latestVersion?: { + version: string; + createdAt: number; + changelog: string; + license?: string | null; + } | null; + versionLicense?: string | null; + owner?: { handle?: string | null; displayName?: string | null; image?: string | null } | null; }) { - const { skill } = result; - console.log(`${skill.slug} ${skill.displayName}`); - if (skill.summary) console.log(`Summary: ${skill.summary}`); - const owner = result.owner?.handle || result.owner?.displayName; - if (owner) console.log(`Owner: ${owner}`); - console.log(`Created: ${formatTimestamp(skill.createdAt)}`); - console.log(`Updated: ${formatTimestamp(skill.updatedAt)}`); - if (result.latestVersion?.version) { - console.log(`Latest: ${result.latestVersion.version}`); - } - console.log( - `License: ${result.versionLicense ?? result.latestVersion?.license ?? PLATFORM_SKILL_LICENSE} (${PLATFORM_SKILL_LICENSE_SUMMARY})`, - ); - const tags = normalizeTags(skill.tags); - const tagEntries = Object.entries(tags); - if (tagEntries.length > 0) { - console.log(`Tags: ${tagEntries.map(([tag, version]) => `${tag}=${version}`).join(", ")}`); - } + const { skill } = result; + console.log(`${skill.slug} ${skill.displayName}`); + if (skill.summary) console.log(`Summary: ${skill.summary}`); + const owner = result.owner?.handle || result.owner?.displayName; + if (owner) console.log(`Owner: ${owner}`); + console.log(`Created: ${formatTimestamp(skill.createdAt)}`); + console.log(`Updated: ${formatTimestamp(skill.updatedAt)}`); + if (result.latestVersion?.version) { + console.log(`Latest: ${result.latestVersion.version}`); + } + console.log( + `License: ${ + result.versionLicense ?? result.latestVersion?.license ?? PLATFORM_SKILL_LICENSE + } (${PLATFORM_SKILL_LICENSE_SUMMARY})` + ); + const tags = normalizeTags(skill.tags); + const tagEntries = Object.entries(tags); + if (tagEntries.length > 0) { + console.log(`Tags: ${tagEntries.map(([tag, version]) => `${tag}=${version}`).join(', ')}`); + } } function printVersionSummary(version: unknown) { - if (!version || typeof version !== "object") return; - const entry = version as { version?: unknown; createdAt?: unknown; changelog?: unknown }; - const value = typeof entry.version === "string" ? entry.version : null; - if (!value) return; - console.log(`Selected: ${value}`); - if (typeof entry.createdAt === "number") { - console.log(`Selected At: ${formatTimestamp(entry.createdAt)}`); - } - if (typeof entry.changelog === "string" && entry.changelog.trim()) { - console.log(`Changelog: ${truncate(entry.changelog, 120)}`); - } + if (!version || typeof version !== 'object') return; + const entry = version as { version?: unknown; createdAt?: unknown; changelog?: unknown }; + const value = typeof entry.version === 'string' ? entry.version : null; + if (!value) return; + console.log(`Selected: ${value}`); + if (typeof entry.createdAt === 'number') { + console.log(`Selected At: ${formatTimestamp(entry.createdAt)}`); + } + if (typeof entry.changelog === 'string' && entry.changelog.trim()) { + console.log(`Changelog: ${truncate(entry.changelog, 120)}`); + } } function printModerationSummary(moderation: unknown) { - const status = normalizeModeration(moderation); - if (!status) return; - const label = status.isMalwareBlocked - ? "MALICIOUS" - : status.isSuspicious - ? "SUSPICIOUS" - : (status.verdict ?? "clean").toUpperCase(); - console.log(`Moderation: ${label}`); - if (status.reasonCodes?.length) { - console.log(`Reasons: ${status.reasonCodes.join(", ")}`); - } - if (status.legacyReason) { - console.log(`Moderation Reason: ${status.legacyReason}`); - } - if (typeof status.updatedAt === "number") { - console.log(`Moderation Updated: ${formatTimestamp(status.updatedAt)}`); - } - if (status.engineVersion) { - console.log(`Moderation Engine: ${status.engineVersion}`); - } - if (status.summary) { - console.log(`Moderation Summary: ${truncate(status.summary, 160)}`); - } - if (status.legacyReason === "quality.low") { - console.log( - "Visibility Guidance: publish a substantive update that passes quality assessment, then re-run inspect.", - ); - } + const status = normalizeModeration(moderation); + if (!status) return; + const label = status.isMalwareBlocked + ? 'MALICIOUS' + : status.isSuspicious + ? 'SUSPICIOUS' + : (status.verdict ?? 'clean').toUpperCase(); + console.log(`Moderation: ${label}`); + if (status.reasonCodes?.length) { + console.log(`Reasons: ${status.reasonCodes.join(', ')}`); + } + if (status.legacyReason) { + console.log(`Moderation Reason: ${status.legacyReason}`); + } + if (typeof status.updatedAt === 'number') { + console.log(`Moderation Updated: ${formatTimestamp(status.updatedAt)}`); + } + if (status.engineVersion) { + console.log(`Moderation Engine: ${status.engineVersion}`); + } + if (status.summary) { + console.log(`Moderation Summary: ${truncate(status.summary, 160)}`); + } + if (status.legacyReason === 'quality.low') { + console.log( + 'Visibility Guidance: publish a substantive update that passes quality assessment, then re-run inspect.' + ); + } } function normalizeModeration(moderation: unknown): ModerationStatus | null { - if (!moderation || typeof moderation !== "object") return null; - const value = moderation as { - isSuspicious?: unknown; - isMalwareBlocked?: unknown; - verdict?: unknown; - reasonCodes?: unknown; - updatedAt?: unknown; - engineVersion?: unknown; - summary?: unknown; - legacyReason?: unknown; - }; - if (typeof value.isSuspicious !== "boolean") return null; - if (typeof value.isMalwareBlocked !== "boolean") return null; - const verdict = - value.verdict === "clean" || value.verdict === "suspicious" || value.verdict === "malicious" - ? value.verdict - : undefined; - const reasonCodes = Array.isArray(value.reasonCodes) - ? value.reasonCodes.filter((reason): reason is string => typeof reason === "string") - : undefined; - return { - isSuspicious: value.isSuspicious, - isMalwareBlocked: value.isMalwareBlocked, - verdict, - reasonCodes, - updatedAt: typeof value.updatedAt === "number" ? value.updatedAt : null, - engineVersion: typeof value.engineVersion === "string" ? value.engineVersion : null, - summary: typeof value.summary === "string" && value.summary.trim() ? value.summary : null, - legacyReason: typeof value.legacyReason === "string" ? value.legacyReason : null, - }; + if (!moderation || typeof moderation !== 'object') return null; + const value = moderation as { + isSuspicious?: unknown; + isMalwareBlocked?: unknown; + verdict?: unknown; + reasonCodes?: unknown; + updatedAt?: unknown; + engineVersion?: unknown; + summary?: unknown; + legacyReason?: unknown; + }; + if (typeof value.isSuspicious !== 'boolean') return null; + if (typeof value.isMalwareBlocked !== 'boolean') return null; + const verdict = + value.verdict === 'clean' || value.verdict === 'suspicious' || value.verdict === 'malicious' + ? value.verdict + : undefined; + const reasonCodes = Array.isArray(value.reasonCodes) + ? value.reasonCodes.filter((reason): reason is string => typeof reason === 'string') + : undefined; + return { + isSuspicious: value.isSuspicious, + isMalwareBlocked: value.isMalwareBlocked, + verdict, + reasonCodes, + updatedAt: typeof value.updatedAt === 'number' ? value.updatedAt : null, + engineVersion: typeof value.engineVersion === 'string' ? value.engineVersion : null, + summary: typeof value.summary === 'string' && value.summary.trim() ? value.summary : null, + legacyReason: typeof value.legacyReason === 'string' ? value.legacyReason : null, + }; } function normalizeTags(tags: unknown): Record { - if (!tags || typeof tags !== "object") return {}; - const entries = Object.entries(tags as Record); - const resolved: Record = {}; - for (const [tag, version] of entries) { - if (typeof version === "string") resolved[tag] = version; - } - return resolved; + if (!tags || typeof tags !== 'object') return {}; + const entries = Object.entries(tags as Record); + const resolved: Record = {}; + for (const [tag, version] of entries) { + if (typeof version === 'string') resolved[tag] = version; + } + return resolved; } function normalizeFiles(files: unknown): FileEntry[] { - if (!Array.isArray(files)) return []; - return files - .map((file) => { - if (!file || typeof file !== "object") return null; - const entry = file as { - path?: unknown; - size?: unknown; - sha256?: unknown; - contentType?: unknown; - }; - if (typeof entry.path !== "string") return null; - const size = typeof entry.size === "number" ? entry.size : Number(entry.size); - const sha256 = typeof entry.sha256 === "string" ? entry.sha256 : null; - const contentType = typeof entry.contentType === "string" ? entry.contentType : null; - return { - path: entry.path, - size: Number.isFinite(size) ? size : null, - sha256, - contentType, - }; - }) - .filter((entry): entry is FileEntry => Boolean(entry)); + if (!Array.isArray(files)) return []; + return files + .map((file) => { + if (!file || typeof file !== 'object') return null; + const entry = file as { + path?: unknown; + size?: unknown; + sha256?: unknown; + contentType?: unknown; + }; + if (typeof entry.path !== 'string') return null; + const size = typeof entry.size === 'number' ? entry.size : Number(entry.size); + const sha256 = typeof entry.sha256 === 'string' ? entry.sha256 : null; + const contentType = typeof entry.contentType === 'string' ? entry.contentType : null; + return { + path: entry.path, + size: Number.isFinite(size) ? size : null, + sha256, + contentType, + }; + }) + .filter((entry): entry is FileEntry => Boolean(entry)); } function formatVersionLine(item: unknown) { - if (!item || typeof item !== "object") return "-"; - const entry = item as { version?: unknown; createdAt?: unknown; changelog?: unknown }; - const version = typeof entry.version === "string" ? entry.version : "?"; - const createdAt = - typeof entry.createdAt === "number" ? formatTimestamp(entry.createdAt) : "unknown"; - const changelog = typeof entry.changelog === "string" ? entry.changelog : ""; - const snippet = changelog ? ` ${truncate(changelog, 80)}` : ""; - return `${version} ${createdAt}${snippet}`; + if (!item || typeof item !== 'object') return '-'; + const entry = item as { version?: unknown; createdAt?: unknown; changelog?: unknown }; + const version = typeof entry.version === 'string' ? entry.version : '?'; + const createdAt = + typeof entry.createdAt === 'number' ? formatTimestamp(entry.createdAt) : 'unknown'; + const changelog = typeof entry.changelog === 'string' ? entry.changelog : ''; + const snippet = changelog ? ` ${truncate(changelog, 80)}` : ''; + return `${version} ${createdAt}${snippet}`; } function printSecuritySummary(version: unknown) { - if (!version || typeof version !== "object") return; - const sec = normalizeSecurity((version as { security?: unknown }).security); - if (!sec) return; - console.log(`Security: ${sec.status.toUpperCase()}`); - if (sec.hasWarnings) { - console.log("Warnings: yes"); - } - if (typeof sec.checkedAt === "number") { - console.log(`Checked: ${formatTimestamp(sec.checkedAt)}`); - } - if (sec.model) { - console.log(`Model: ${sec.model}`); - } + if (!version || typeof version !== 'object') return; + const sec = normalizeSecurity((version as { security?: unknown }).security); + if (!sec) return; + console.log(`Security: ${sec.status.toUpperCase()}`); + if (sec.hasWarnings) { + console.log('Warnings: yes'); + } + if (typeof sec.checkedAt === 'number') { + console.log(`Checked: ${formatTimestamp(sec.checkedAt)}`); + } + if (sec.model) { + console.log(`Model: ${sec.model}`); + } } function normalizeSecurity(security: unknown): SecurityStatus | null { - if (!security || typeof security !== "object") return null; - const value = security as { - status?: unknown; - hasWarnings?: unknown; - checkedAt?: unknown; - model?: unknown; - }; - if ( - value.status !== "clean" && - value.status !== "suspicious" && - value.status !== "malicious" && - value.status !== "pending" && - value.status !== "error" - ) { - return null; - } - if (typeof value.hasWarnings !== "boolean") return null; - const checkedAt = typeof value.checkedAt === "number" ? value.checkedAt : null; - const model = typeof value.model === "string" ? value.model : null; - return { - status: value.status, - hasWarnings: value.hasWarnings, - checkedAt, - model, - }; + if (!security || typeof security !== 'object') return null; + const value = security as { + status?: unknown; + hasWarnings?: unknown; + checkedAt?: unknown; + model?: unknown; + }; + if ( + value.status !== 'clean' && + value.status !== 'suspicious' && + value.status !== 'malicious' && + value.status !== 'pending' && + value.status !== 'error' + ) { + return null; + } + if (typeof value.hasWarnings !== 'boolean') return null; + const checkedAt = typeof value.checkedAt === 'number' ? value.checkedAt : null; + const model = typeof value.model === 'string' ? value.model : null; + return { + status: value.status, + hasWarnings: value.hasWarnings, + checkedAt, + model, + }; } function formatFileLine(file: FileEntry) { - const size = file.size === null ? "?" : formatBytes(file.size); - const sha = file.sha256 ?? "?"; - const type = file.contentType ? ` ${file.contentType}` : ""; - return `${file.path} ${size} ${sha}${type}`; + const size = file.size === null ? '?' : formatBytes(file.size); + const sha = file.sha256 ?? '?'; + const type = file.contentType ? ` ${file.contentType}` : ''; + return `${file.path} ${size} ${sha}${type}`; } function formatTimestamp(timestamp: number) { - if (!Number.isFinite(timestamp)) return "unknown"; - return new Date(timestamp).toISOString(); + if (!Number.isFinite(timestamp)) return 'unknown'; + return new Date(timestamp).toISOString(); } function formatBytes(bytes: number) { - if (!Number.isFinite(bytes)) return "?"; - const units = ["B", "KB", "MB", "GB"]; - let value = bytes; - let index = 0; - while (value >= 1024 && index < units.length - 1) { - value /= 1024; - index += 1; - } - const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10; - return `${rounded}${units[index]}`; + if (!Number.isFinite(bytes)) return '?'; + const units = ['B', 'KB', 'MB', 'GB']; + let value = bytes; + let index = 0; + while (value >= 1024 && index < units.length - 1) { + value /= 1024; + index += 1; + } + const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10; + return `${rounded}${units[index]}`; } function clampLimit(limit: number, fallback: number) { - if (!Number.isFinite(limit)) return fallback; - return Math.min(Math.max(1, Math.round(limit)), 200); + if (!Number.isFinite(limit)) return fallback; + return Math.min(Math.max(1, Math.round(limit)), 200); } function truncate(str: string, maxLen: number) { - if (str.length <= maxLen) return str; - return `${str.slice(0, maxLen - 3)}...`; + if (str.length <= maxLen) return str; + return `${str.slice(0, maxLen - 3)}...`; } diff --git a/dt-skill/src/cli/commands/publish.test.ts b/dt-skill/src/cli/commands/publish.test.ts index f6b66a6a..c5467bcd 100644 --- a/dt-skill/src/cli/commands/publish.test.ts +++ b/dt-skill/src/cli/commands/publish.test.ts @@ -1,15 +1,16 @@ /* @vitest-environment node */ -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; -import { MAX_CLAWSCAN_NOTE_CHARS } from "../../schema/index.js"; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from '../../../test/cliCommandTestKit.js'; +import { MAX_CLAWSCAN_NOTE_CHARS } from '../../schema/index.js'; const registryMocks = createRegistryModuleMocks(); const httpMocks = createHttpModuleMocks(); @@ -17,491 +18,491 @@ const uiMocks = createUiModuleMocks({ interactive: true }); const mockSearchMultiselect = vi.fn(); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../ui.js", () => uiMocks.moduleFactory()); -vi.mock("../prompts/search-multiselect.js", async () => { - const actual = await vi.importActual("../prompts/search-multiselect.js"); - return { - ...actual, - searchMultiselect: (opts: any) => mockSearchMultiselect(opts), - }; +vi.mock('../registry.js', () => registryMocks.moduleFactory()); +vi.mock('../../http.js', () => httpMocks.moduleFactory()); +vi.mock('../ui.js', () => uiMocks.moduleFactory()); +vi.mock('../prompts/search-multiselect.js', async () => { + const actual = await vi.importActual('../prompts/search-multiselect.js'); + return { + ...actual, + searchMultiselect: (opts: any) => mockSearchMultiselect(opts), + }; }); -const { cmdPublish } = await import("./publish"); +const { cmdPublish } = await import('./publish'); async function makeTmpWorkdir() { - const root = await mkdtemp(join(tmpdir(), "dt-skill-publish-")); - return root; + const root = await mkdtemp(join(tmpdir(), 'dt-skill-publish-')); + return root; } function makeOpts(workdir: string) { - return makeGlobalOpts(workdir); + return makeGlobalOpts(workdir); } afterEach(() => { - vi.restoreAllMocks(); - vi.clearAllMocks(); + vi.restoreAllMocks(); + vi.clearAllMocks(); }); -describe("cmdPublish", () => { - it("publishes SKILL.md from disk (mocked HTTP)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "my-skill"); - await mkdir(folder, { recursive: true }); - const skillContent = "# Skill\n\nHello\n"; - const notesContent = "notes\n"; - await writeFile(join(folder, "SKILL.md"), skillContent, "utf8"); - await writeFile(join(folder, "notes.md"), notesContent, "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - skillId: "skill_1", - versionId: "ver_1", - }); - - await cmdPublish(makeOpts(workdir), "my-skill", { - slug: "my-skill", - name: "My Skill", - version: "1.0.0", - changelog: "", - tags: "latest", - clawscanNote: "This skill needs network access to call the user's configured API.", - }); - - const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/v1/skills"; - }); - if (!publishCall) throw new Error("Missing publish call"); - expect(publishCall[1]).not.toHaveProperty("token"); - const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; - const payloadEntry = publishForm.get("payload"); - if (typeof payloadEntry !== "string") throw new Error("Missing publish payload"); - const payload = JSON.parse(payloadEntry); - expect(payload.slug).toBe("my-skill"); - expect(payload.displayName).toBe("My Skill"); - expect(payload.version).toBe("1.0.0"); - expect(payload.changelog).toBe(""); - expect(payload.clawScanNote).toBe( - "This skill needs network access to call the user's configured API.", - ); - expect(payload.acceptLicenseTerms).toBe(true); - expect(payload.tags).toEqual(["latest"]); - const files = publishForm.getAll("files") as Array; - expect(files.map((file) => file.name ?? "").sort()).toEqual(["SKILL.md", "notes.md"]); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("发布时同时上传文本文件和二进制资源", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "skill-with-assets"); - await mkdir(join(folder, "assets"), { recursive: true }); - await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); - await writeFile(join(folder, "assets", "logo.png"), new Uint8Array([0, 1, 2, 255])); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - skillId: "skill_1", - versionId: "ver_1", - }); - - await cmdPublish(makeOpts(workdir), "skill-with-assets", { - version: "1.0.0", - }); - - const publishCall = httpMocks.apiRequestForm.mock.calls[0]; - const publishForm = (publishCall?.[1] as { form?: FormData }).form as FormData; - const files = publishForm.getAll("files") as Array; - expect(files.map((file) => file.name ?? "").sort()).toEqual([ - "SKILL.md", - "assets/logo.png", - ]); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects oversized clawscan notes before uploading skill files", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "oversized-note"); - await mkdir(folder, { recursive: true }); - await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); - - await expect( - cmdPublish(makeOpts(workdir), "oversized-note", { - slug: "oversized-note", - name: "Oversized Note", - version: "1.0.0", - clawscanNote: "x".repeat(MAX_CLAWSCAN_NOTE_CHARS + 1), - }), - ).rejects.toThrow(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("allows empty changelog when updating an existing skill", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "existing-skill"); - await mkdir(folder, { recursive: true }); - await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - skillId: "skill_1", - versionId: "ver_2", - }); - - await cmdPublish(makeOpts(workdir), "existing-skill", { - version: "1.0.1", - changelog: "", - tags: "latest", - }); - - expect(httpMocks.apiRequestForm).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ path: "/api/v1/skills", method: "POST" }), - expect.anything(), - ); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("still publishes a root SKILL.md hidden by broad ignore patterns", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "ignored-manifest"); - await mkdir(folder, { recursive: true }); - await writeFile(join(folder, ".gitignore"), "*.md\n", "utf8"); - await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); - await writeFile(join(folder, "notes.md"), "ignored notes\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - skillId: "skill_1", - versionId: "ver_1", - }); - - await cmdPublish(makeOpts(workdir), "ignored-manifest", { - slug: "ignored-manifest", - name: "Ignored Manifest", - version: "1.0.0", - changelog: "", - tags: "latest", - }); - - const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/v1/skills"; - }); - if (!publishCall) throw new Error("Missing publish call"); - const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; - const files = publishForm.getAll("files") as Array; - expect(files.map((file) => file.name ?? "")).toEqual(["SKILL.md"]); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("includes owner handle for org-owned skill publishes", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "org-skill"); - await mkdir(folder, { recursive: true }); - await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - ok: true, - skillId: "skill_1", - versionId: "ver_2", - }); - - await cmdPublish(makeOpts(workdir), "org-skill", { - owner: "@openclaw", - migrateOwner: true, - version: "1.0.1", - changelog: "", - tags: "latest", - }); - - const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/v1/skills"; - }); - if (!publishCall) throw new Error("Missing publish call"); - const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; - const payloadEntry = publishForm.get("payload"); - if (typeof payloadEntry !== "string") throw new Error("Missing publish payload"); - const payload = JSON.parse(payloadEntry); - expect(payload.ownerHandle).toBe("openclaw"); - expect(payload.migrateOwner).toBe(true); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - it("rejects plugin folders with guidance to use a skill folder", async () => { - const workdir = await makeTmpWorkdir(); - try { - const folder = join(workdir, "demo-plugin"); - await mkdir(folder, { recursive: true }); - await writeFile( - join(folder, "package.json"), - JSON.stringify({ name: "demo-plugin", openclaw: { extensions: ["./index.ts"] } }), - "utf8", - ); - await writeFile(join(folder, "openclaw.plugin.json"), '{"id":"demo-plugin"}', "utf8"); - - await expect( - cmdPublish(makeOpts(workdir), "demo-plugin", { - slug: "demo-plugin", - name: "Demo Plugin", - version: "1.0.0", - tags: "latest", - }), - ).rejects.toThrow( - "This folder looks like a code plugin, not a skill. Use a folder with SKILL.md.", - ); - expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - - describe("cmdPublish batch mode", () => { - it("detects multiple skill folders and switches to batch upload (T004)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const skillsDir = join(workdir, "skills-batch"); - await mkdir(join(skillsDir, "skill-a"), { recursive: true }); - await mkdir(join(skillsDir, "skill-b"), { recursive: true }); - await writeFile(join(skillsDir, "skill-a", "SKILL.md"), "# Skill A\n", "utf8"); - await writeFile(join(skillsDir, "skill-b", "SKILL.md"), "# Skill B\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - success: true, - data: { - importedCount: 2, - refreshedCount: 2, - importedSkills: [ - { slug: "skill-a", name: "skill-a" }, - { slug: "skill-b", name: "skill-b" }, - ], - }, - }); - - await cmdPublish(makeOpts(workdir), "skills-batch", { - all: true, - tags: "latest", - }); - - // Should call /api/skills/import-file, NOT /api/v1/skills - const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/skills/import-file"; - }); - expect(batchCall).toBeDefined(); - - // Should send packageName derived from folder basename - const form = (batchCall![1] as { form?: FormData }).form as FormData; - const packageName = form.get("packageName"); - expect(packageName).toBe("skills-batch"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } +describe('cmdPublish', () => { + it('publishes SKILL.md from disk (mocked HTTP)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'my-skill'); + await mkdir(folder, { recursive: true }); + const skillContent = '# Skill\n\nHello\n'; + const notesContent = 'notes\n'; + await writeFile(join(folder, 'SKILL.md'), skillContent, 'utf8'); + await writeFile(join(folder, 'notes.md'), notesContent, 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_1', + versionId: 'ver_1', + }); + + await cmdPublish(makeOpts(workdir), 'my-skill', { + slug: 'my-skill', + name: 'My Skill', + version: '1.0.0', + changelog: '', + tags: 'latest', + clawscanNote: "This skill needs network access to call the user's configured API.", + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + expect(publishCall[1]).not.toHaveProperty('token'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.slug).toBe('my-skill'); + expect(payload.displayName).toBe('My Skill'); + expect(payload.version).toBe('1.0.0'); + expect(payload.changelog).toBe(''); + expect(payload.clawScanNote).toBe( + "This skill needs network access to call the user's configured API." + ); + expect(payload.acceptLicenseTerms).toBe(true); + expect(payload.tags).toEqual(['latest']); + const files = publishForm.getAll('files') as Array; + expect(files.map((file) => file.name ?? '').sort()).toEqual(['SKILL.md', 'notes.md']); + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - it("packs selected skills into ZIP with correct structure (T005)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const skillsDir = join(workdir, "zip-test"); - await mkdir(join(skillsDir, "alpha"), { recursive: true }); - await mkdir(join(skillsDir, "beta"), { recursive: true }); - await writeFile(join(skillsDir, "alpha", "SKILL.md"), "# Alpha\n", "utf8"); - await writeFile(join(skillsDir, "alpha", "config.json"), "{\"a\":1}", "utf8"); - await writeFile(join(skillsDir, "beta", "SKILL.md"), "# Beta\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - success: true, - data: { - importedCount: 2, - refreshedCount: 2, - importedSkills: [ - { slug: "alpha", name: "alpha" }, - { slug: "beta", name: "beta" }, - ], - }, - }); + it('发布时同时上传文本文件和二进制资源', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'skill-with-assets'); + await mkdir(join(folder, 'assets'), { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# Skill\n', 'utf8'); + await writeFile(join(folder, 'assets', 'logo.png'), new Uint8Array([0, 1, 2, 255])); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_1', + versionId: 'ver_1', + }); + + await cmdPublish(makeOpts(workdir), 'skill-with-assets', { + version: '1.0.0', + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls[0]; + const publishForm = (publishCall?.[1] as { form?: FormData }).form as FormData; + const files = publishForm.getAll('files') as Array; + expect(files.map((file) => file.name ?? '').sort()).toEqual([ + 'SKILL.md', + 'assets/logo.png', + ]); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - await cmdPublish(makeOpts(workdir), "zip-test", { all: true }); + it('rejects oversized clawscan notes before uploading skill files', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'oversized-note'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# Skill\n', 'utf8'); + + await expect( + cmdPublish(makeOpts(workdir), 'oversized-note', { + slug: 'oversized-note', + name: 'Oversized Note', + version: '1.0.0', + clawscanNote: 'x'.repeat(MAX_CLAWSCAN_NOTE_CHARS + 1), + }) + ).rejects.toThrow( + `ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.` + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/skills/import-file"; - }); - expect(batchCall).toBeDefined(); - const form = (batchCall![1] as { form?: FormData }).form as FormData; - const fileEntry = form.get("file"); - expect(fileEntry).toBeDefined(); - - // Verify ZIP contents - const AdmZip = (await import("adm-zip")).default; - const arrayBuffer = await (fileEntry as Blob).arrayBuffer(); - const zip = new AdmZip(Buffer.from(arrayBuffer)); - const entries = zip.getEntries().map((e: any) => e.entryName); - expect(entries.some((n: string) => n.includes("alpha"))).toBe(true); - expect(entries.some((n: string) => n.includes("beta"))).toBe(true); - expect(entries.some((n: string) => n.includes("SKILL.md"))).toBe(true); - - // ZIP filename should use folder basename - const batchCall2 = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/skills/import-file"; - }); - const form2 = (batchCall2![1] as { form?: FormData }).form as FormData; - const packageName2 = form2.get("packageName"); - expect(packageName2).toBe("zip-test"); - } finally { - await rm(workdir, { recursive: true, force: true }); - } + it('allows empty changelog when updating an existing skill', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'existing-skill'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# Skill\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_1', + versionId: 'ver_2', + }); + + await cmdPublish(makeOpts(workdir), 'existing-skill', { + version: '1.0.1', + changelog: '', + tags: 'latest', + }); + + expect(httpMocks.apiRequestForm).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ path: '/api/v1/skills', method: 'POST' }), + expect.anything() + ); + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - it("calls /api/skills/import-file for batch, not /api/v1/skills (T006)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const skillsDir = join(workdir, "api-check"); - await mkdir(join(skillsDir, "x-skill"), { recursive: true }); - await mkdir(join(skillsDir, "y-skill"), { recursive: true }); - await writeFile(join(skillsDir, "x-skill", "SKILL.md"), "# X\n", "utf8"); - await writeFile(join(skillsDir, "y-skill", "SKILL.md"), "# Y\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - success: true, - data: { - importedCount: 2, - refreshedCount: 2, - importedSkills: [], - }, - }); + it('still publishes a root SKILL.md hidden by broad ignore patterns', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'ignored-manifest'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, '.gitignore'), '*.md\n', 'utf8'); + await writeFile(join(folder, 'SKILL.md'), '# Skill\n', 'utf8'); + await writeFile(join(folder, 'notes.md'), 'ignored notes\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_1', + versionId: 'ver_1', + }); + + await cmdPublish(makeOpts(workdir), 'ignored-manifest', { + slug: 'ignored-manifest', + name: 'Ignored Manifest', + version: '1.0.0', + changelog: '', + tags: 'latest', + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const files = publishForm.getAll('files') as Array; + expect(files.map((file) => file.name ?? '')).toEqual(['SKILL.md']); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - await cmdPublish(makeOpts(workdir), "api-check", { all: true }); + it('includes owner handle for org-owned skill publishes', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'org-skill'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# Skill\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + ok: true, + skillId: 'skill_1', + versionId: 'ver_2', + }); + + await cmdPublish(makeOpts(workdir), 'org-skill', { + owner: '@openclaw', + migrateOwner: true, + version: '1.0.1', + changelog: '', + tags: 'latest', + }); + + const publishCall = httpMocks.apiRequestForm.mock.calls.find((call) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + if (!publishCall) throw new Error('Missing publish call'); + const publishForm = (publishCall[1] as { form?: FormData }).form as FormData; + const payloadEntry = publishForm.get('payload'); + if (typeof payloadEntry !== 'string') throw new Error('Missing publish payload'); + const payload = JSON.parse(payloadEntry); + expect(payload.ownerHandle).toBe('openclaw'); + expect(payload.migrateOwner).toBe(true); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - const v1Call = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/v1/skills"; - }); - expect(v1Call).toBeUndefined(); // Should NOT call /api/v1/skills in batch mode - } finally { - await rm(workdir, { recursive: true, force: true }); - } + it('rejects plugin folders with guidance to use a skill folder', async () => { + const workdir = await makeTmpWorkdir(); + try { + const folder = join(workdir, 'demo-plugin'); + await mkdir(folder, { recursive: true }); + await writeFile( + join(folder, 'package.json'), + JSON.stringify({ name: 'demo-plugin', openclaw: { extensions: ['./index.ts'] } }), + 'utf8' + ); + await writeFile(join(folder, 'openclaw.plugin.json'), '{"id":"demo-plugin"}', 'utf8'); + + await expect( + cmdPublish(makeOpts(workdir), 'demo-plugin', { + slug: 'demo-plugin', + name: 'Demo Plugin', + version: '1.0.0', + tags: 'latest', + }) + ).rejects.toThrow( + 'This folder looks like a code plugin, not a skill. Use a folder with SKILL.md.' + ); + expect(httpMocks.apiRequestForm).not.toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - it("uses searchMultiselect in interactive mode and only uploads selected (T011)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const skillsDir = join(workdir, "interactive"); - await mkdir(join(skillsDir, "s1"), { recursive: true }); - await mkdir(join(skillsDir, "s2"), { recursive: true }); - await mkdir(join(skillsDir, "s3"), { recursive: true }); - await writeFile(join(skillsDir, "s1", "SKILL.md"), "# S1\n", "utf8"); - await writeFile(join(skillsDir, "s2", "SKILL.md"), "# S2\n", "utf8"); - await writeFile(join(skillsDir, "s3", "SKILL.md"), "# S3\n", "utf8"); - - // User selects only s1 and s3 - mockSearchMultiselect.mockResolvedValue(["s1", "s3"]); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - success: true, - data: { - importedCount: 2, - refreshedCount: 2, - importedSkills: [ - { slug: "s1", name: "s1" }, - { slug: "s3", name: "s3" }, - ], - }, + describe('cmdPublish batch mode', () => { + it('detects multiple skill folders and switches to batch upload (T004)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, 'skills-batch'); + await mkdir(join(skillsDir, 'skill-a'), { recursive: true }); + await mkdir(join(skillsDir, 'skill-b'), { recursive: true }); + await writeFile(join(skillsDir, 'skill-a', 'SKILL.md'), '# Skill A\n', 'utf8'); + await writeFile(join(skillsDir, 'skill-b', 'SKILL.md'), '# Skill B\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: 'skill-a', name: 'skill-a' }, + { slug: 'skill-b', name: 'skill-b' }, + ], + }, + }); + + await cmdPublish(makeOpts(workdir), 'skills-batch', { + all: true, + tags: 'latest', + }); + + // Should call /api/skills/import-file, NOT /api/v1/skills + const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/skills/import-file'; + }); + expect(batchCall).toBeDefined(); + + // Should send packageName derived from folder basename + const form = (batchCall![1] as { form?: FormData }).form as FormData; + const packageName = form.get('packageName'); + expect(packageName).toBe('skills-batch'); + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - await cmdPublish(makeOpts(workdir), "interactive", { tags: "latest" }); - - expect(mockSearchMultiselect).toHaveBeenCalledWith( - expect.objectContaining({ message: expect.stringContaining("3 found") }), - ); - - const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { - const req = call[1] as { path?: string } | undefined; - return req?.path === "/api/skills/import-file"; + it('packs selected skills into ZIP with correct structure (T005)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, 'zip-test'); + await mkdir(join(skillsDir, 'alpha'), { recursive: true }); + await mkdir(join(skillsDir, 'beta'), { recursive: true }); + await writeFile(join(skillsDir, 'alpha', 'SKILL.md'), '# Alpha\n', 'utf8'); + await writeFile(join(skillsDir, 'alpha', 'config.json'), '{"a":1}', 'utf8'); + await writeFile(join(skillsDir, 'beta', 'SKILL.md'), '# Beta\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: 'alpha', name: 'alpha' }, + { slug: 'beta', name: 'beta' }, + ], + }, + }); + + await cmdPublish(makeOpts(workdir), 'zip-test', { all: true }); + + const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/skills/import-file'; + }); + expect(batchCall).toBeDefined(); + const form = (batchCall![1] as { form?: FormData }).form as FormData; + const fileEntry = form.get('file'); + expect(fileEntry).toBeDefined(); + + // Verify ZIP contents + const AdmZip = (await import('adm-zip')).default; + const arrayBuffer = await (fileEntry as Blob).arrayBuffer(); + const zip = new AdmZip(Buffer.from(arrayBuffer)); + const entries = zip.getEntries().map((e: any) => e.entryName); + expect(entries.some((n: string) => n.includes('alpha'))).toBe(true); + expect(entries.some((n: string) => n.includes('beta'))).toBe(true); + expect(entries.some((n: string) => n.includes('SKILL.md'))).toBe(true); + + // ZIP filename should use folder basename + const batchCall2 = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/skills/import-file'; + }); + const form2 = (batchCall2![1] as { form?: FormData }).form as FormData; + const packageName2 = form2.get('packageName'); + expect(packageName2).toBe('zip-test'); + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - expect(batchCall).toBeDefined(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); - it("reports imported count and skill list on success (T014)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const skillsDir = join(workdir, "report"); - await mkdir(join(skillsDir, "r1"), { recursive: true }); - await mkdir(join(skillsDir, "r2"), { recursive: true }); - await writeFile(join(skillsDir, "r1", "SKILL.md"), "# R1\n", "utf8"); - await writeFile(join(skillsDir, "r2", "SKILL.md"), "# R2\n", "utf8"); - - httpMocks.apiRequestForm.mockResolvedValueOnce({ - success: true, - data: { - importedCount: 2, - refreshedCount: 2, - importedSkills: [ - { slug: "r1", name: "r1" }, - { slug: "r2", name: "r2" }, - ], - }, + it('calls /api/skills/import-file for batch, not /api/v1/skills (T006)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, 'api-check'); + await mkdir(join(skillsDir, 'x-skill'), { recursive: true }); + await mkdir(join(skillsDir, 'y-skill'), { recursive: true }); + await writeFile(join(skillsDir, 'x-skill', 'SKILL.md'), '# X\n', 'utf8'); + await writeFile(join(skillsDir, 'y-skill', 'SKILL.md'), '# Y\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [], + }, + }); + + await cmdPublish(makeOpts(workdir), 'api-check', { all: true }); + + const v1Call = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/v1/skills'; + }); + expect(v1Call).toBeUndefined(); // Should NOT call /api/v1/skills in batch mode + } finally { + await rm(workdir, { recursive: true, force: true }); + } }); - const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); - - await cmdPublish(makeOpts(workdir), "report", { all: true }); - - expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( - expect.stringContaining("2 skill(s)"), - ); - expect(mockLog).toHaveBeenCalledWith( - expect.stringContaining("r1"), - ); + it('uses searchMultiselect in interactive mode and only uploads selected (T011)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, 'interactive'); + await mkdir(join(skillsDir, 's1'), { recursive: true }); + await mkdir(join(skillsDir, 's2'), { recursive: true }); + await mkdir(join(skillsDir, 's3'), { recursive: true }); + await writeFile(join(skillsDir, 's1', 'SKILL.md'), '# S1\n', 'utf8'); + await writeFile(join(skillsDir, 's2', 'SKILL.md'), '# S2\n', 'utf8'); + await writeFile(join(skillsDir, 's3', 'SKILL.md'), '# S3\n', 'utf8'); + + // User selects only s1 and s3 + mockSearchMultiselect.mockResolvedValue(['s1', 's3']); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: 's1', name: 's1' }, + { slug: 's3', name: 's3' }, + ], + }, + }); + + await cmdPublish(makeOpts(workdir), 'interactive', { tags: 'latest' }); + + expect(mockSearchMultiselect).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining('3 found') }) + ); + + const batchCall = httpMocks.apiRequestForm.mock.calls.find((call: any[]) => { + const req = call[1] as { path?: string } | undefined; + return req?.path === '/api/skills/import-file'; + }); + expect(batchCall).toBeDefined(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - mockLog.mockRestore(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } - }); + it('reports imported count and skill list on success (T014)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, 'report'); + await mkdir(join(skillsDir, 'r1'), { recursive: true }); + await mkdir(join(skillsDir, 'r2'), { recursive: true }); + await writeFile(join(skillsDir, 'r1', 'SKILL.md'), '# R1\n', 'utf8'); + await writeFile(join(skillsDir, 'r2', 'SKILL.md'), '# R2\n', 'utf8'); + + httpMocks.apiRequestForm.mockResolvedValueOnce({ + success: true, + data: { + importedCount: 2, + refreshedCount: 2, + importedSkills: [ + { slug: 'r1', name: 'r1' }, + { slug: 'r2', name: 'r2' }, + ], + }, + }); + + const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + + await cmdPublish(makeOpts(workdir), 'report', { all: true }); + + expect(uiMocks.spinner.succeed).toHaveBeenCalledWith( + expect.stringContaining('2 skill(s)') + ); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining('r1')); + + mockLog.mockRestore(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); - it("shows error on upload failure (T015)", async () => { - const workdir = await makeTmpWorkdir(); - try { - const skillsDir = join(workdir, "fail-test"); - await mkdir(join(skillsDir, "f1"), { recursive: true }); - await writeFile(join(skillsDir, "f1", "SKILL.md"), "# F1\n", "utf8"); + it('shows error on upload failure (T015)', async () => { + const workdir = await makeTmpWorkdir(); + try { + const skillsDir = join(workdir, 'fail-test'); + await mkdir(join(skillsDir, 'f1'), { recursive: true }); + await writeFile(join(skillsDir, 'f1', 'SKILL.md'), '# F1\n', 'utf8'); - httpMocks.apiRequestForm.mockRejectedValueOnce(new Error("Network error")); + httpMocks.apiRequestForm.mockRejectedValueOnce(new Error('Network error')); - await expect( - cmdPublish(makeOpts(workdir), "fail-test", { all: true }), - ).rejects.toThrow("Network error"); + await expect( + cmdPublish(makeOpts(workdir), 'fail-test', { all: true }) + ).rejects.toThrow('Network error'); - expect(uiMocks.spinner.fail).toHaveBeenCalled(); - } finally { - await rm(workdir, { recursive: true, force: true }); - } + expect(uiMocks.spinner.fail).toHaveBeenCalled(); + } finally { + await rm(workdir, { recursive: true, force: true }); + } + }); }); - }); }); diff --git a/dt-skill/src/cli/commands/publish.ts b/dt-skill/src/cli/commands/publish.ts index 16c1ae48..d81875d7 100644 --- a/dt-skill/src/cli/commands/publish.ts +++ b/dt-skill/src/cli/commands/publish.ts @@ -1,299 +1,297 @@ -import { readFile, readdir, stat } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; -import AdmZip from "adm-zip"; -import semver from "semver"; -import { apiRequestForm } from "../../http.js"; +import { readFile, readdir, stat } from 'node:fs/promises'; +import { basename, join, resolve } from 'node:path'; +import AdmZip from 'adm-zip'; +import semver from 'semver'; +import { apiRequestForm } from '../../http.js'; import { - ApiRoutes, - ApiV1PublishResponseSchema, - normalizeClawScanNote, -} from "../../schema/index.js"; -import { listPublishFiles } from "../../skills.js"; -import { getRegistry } from "../registry.js"; -import { sanitizeSlug, titleCase } from "../slug.js"; -import { findSkillFolders } from "../scanSkills.js"; -import { searchMultiselect } from "../prompts/search-multiselect.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive } from "../ui.js"; + ApiRoutes, + ApiV1PublishResponseSchema, + normalizeClawScanNote, +} from '../../schema/index.js'; +import { listPublishFiles } from '../../skills.js'; +import { getRegistry } from '../registry.js'; +import { sanitizeSlug, titleCase } from '../slug.js'; +import { findSkillFolders } from '../scanSkills.js'; +import { searchMultiselect } from '../prompts/search-multiselect.js'; +import type { GlobalOpts } from '../types.js'; +import { createSpinner, fail, formatError, isInteractive } from '../ui.js'; export async function cmdPublish( - opts: GlobalOpts, - folderArg: string, - options: { - slug?: string; - name?: string; - owner?: string; - version?: string; - changelog?: string; - tags?: string; - forkOf?: string; - clawscanNote?: string; - migrateOwner?: boolean; - all?: boolean; - category?: string; - }, -) { - // Resolve folder path: try workdir first (standard behavior), - // but fall back to cwd so relative paths work from whichever directory - // the user runs the command. - const folder = folderArg - ? await resolveFolderPath(opts.workdir, folderArg) - : null; - if (!folder) fail("Path required"); - const folderStat = await stat(folder).catch(() => null); - if (!folderStat || !folderStat.isDirectory()) fail("Path must be a folder"); - if (await looksLikePluginFolder(folder)) { - fail("This folder looks like a code plugin, not a skill. Use a folder with SKILL.md."); - } - - // Detect batch mode: if folder does NOT contain SKILL.md directly, - // but contains subdirectories with SKILL.md, switch to batch upload - const directSkillMd = await stat(join(folder, "SKILL.md")).catch(() => null); - if (!directSkillMd?.isFile()) { - const skillFolders = await findSkillFolders(folder); - if (skillFolders.length > 0) { - return cmdPublishBatch(opts, folder, skillFolders, options); + opts: GlobalOpts, + folderArg: string, + options: { + slug?: string; + name?: string; + owner?: string; + version?: string; + changelog?: string; + tags?: string; + forkOf?: string; + clawscanNote?: string; + migrateOwner?: boolean; + all?: boolean; + category?: string; } - } - - // Single skill mode (existing logic) - const registry = await getRegistry(opts, { cache: true }); - - const slug = options.slug ?? sanitizeSlug(basename(folder)); - const displayName = options.name ?? titleCase(basename(folder)); - const ownerHandle = options.owner?.trim().replace(/^@+/, ""); - const version = options.version; - const changelog = options.changelog ?? ""; - let clawScanNote: string | undefined; - try { - clawScanNote = normalizeClawScanNote(options.clawscanNote); - } catch (error) { - fail(formatError(error)); - } - const tagsValue = options.tags ?? "latest"; - const tags = tagsValue - .split(",") - .map((tag) => tag.trim()) - .filter(Boolean); - - const forkOfRaw = options.forkOf?.trim(); - const forkOf = forkOfRaw ? parseForkOf(forkOfRaw) : undefined; - - if (!slug) fail("--slug required"); - if (!displayName) fail("--name required"); - if (!version || !semver.valid(version)) fail("--version must be valid semver"); - - const spinner = createSpinner(`Preparing ${slug}@${version}`); - try { - const filesOnDisk = await ensureRootManifestFile(folder, await listPublishFiles(folder)); - if (filesOnDisk.length === 0) fail("No files found"); - if ( - !filesOnDisk.some((file) => { - const lower = file.relPath.toLowerCase(); - return lower === "skill.md" || lower === "skills.md"; - }) - ) { - fail("SKILL.md required"); +) { + // Resolve folder path: try workdir first (standard behavior), + // but fall back to cwd so relative paths work from whichever directory + // the user runs the command. + const folder = folderArg ? await resolveFolderPath(opts.workdir, folderArg) : null; + if (!folder) fail('Path required'); + const folderStat = await stat(folder).catch(() => null); + if (!folderStat || !folderStat.isDirectory()) fail('Path must be a folder'); + if (await looksLikePluginFolder(folder)) { + fail('This folder looks like a code plugin, not a skill. Use a folder with SKILL.md.'); } - const form = new FormData(); - form.set( - "payload", - JSON.stringify({ - slug, - displayName, - ...(ownerHandle ? { ownerHandle } : {}), - ...(options.migrateOwner ? { migrateOwner: true } : {}), - version, - changelog, - ...(clawScanNote ? { clawScanNote } : {}), - acceptLicenseTerms: true, - tags, - ...(forkOf ? { forkOf } : {}), - }), - ); - - let index = 0; - for (const file of filesOnDisk) { - index += 1; - spinner.text = `Uploading ${file.relPath} (${index}/${filesOnDisk.length})`; - const blob = new Blob([Buffer.from(file.bytes)], { type: file.contentType ?? "text/plain" }); - form.append("files", blob, file.relPath); + // Detect batch mode: if folder does NOT contain SKILL.md directly, + // but contains subdirectories with SKILL.md, switch to batch upload + const directSkillMd = await stat(join(folder, 'SKILL.md')).catch(() => null); + if (!directSkillMd?.isFile()) { + const skillFolders = await findSkillFolders(folder); + if (skillFolders.length > 0) { + return cmdPublishBatch(opts, folder, skillFolders, options); + } } - spinner.text = `Publishing ${slug}@${version}`; - const result = await apiRequestForm( - registry, - { method: "POST", path: ApiRoutes.skills, form }, - ApiV1PublishResponseSchema, - ); - - spinner.succeed(`OK. Published ${slug}@${version} (${result.versionId})`); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + // Single skill mode (existing logic) + const registry = await getRegistry(opts, { cache: true }); + + const slug = options.slug ?? sanitizeSlug(basename(folder)); + const displayName = options.name ?? titleCase(basename(folder)); + const ownerHandle = options.owner?.trim().replace(/^@+/, ''); + const version = options.version; + const changelog = options.changelog ?? ''; + let clawScanNote: string | undefined; + try { + clawScanNote = normalizeClawScanNote(options.clawscanNote); + } catch (error) { + fail(formatError(error)); + } + const tagsValue = options.tags ?? 'latest'; + const tags = tagsValue + .split(',') + .map((tag) => tag.trim()) + .filter(Boolean); + + const forkOfRaw = options.forkOf?.trim(); + const forkOf = forkOfRaw ? parseForkOf(forkOfRaw) : undefined; + + if (!slug) fail('--slug required'); + if (!displayName) fail('--name required'); + if (!version || !semver.valid(version)) fail('--version must be valid semver'); + + const spinner = createSpinner(`Preparing ${slug}@${version}`); + try { + const filesOnDisk = await ensureRootManifestFile(folder, await listPublishFiles(folder)); + if (filesOnDisk.length === 0) fail('No files found'); + if ( + !filesOnDisk.some((file) => { + const lower = file.relPath.toLowerCase(); + return lower === 'skill.md' || lower === 'skills.md'; + }) + ) { + fail('SKILL.md required'); + } + + const form = new FormData(); + form.set( + 'payload', + JSON.stringify({ + slug, + displayName, + ...(ownerHandle ? { ownerHandle } : {}), + ...(options.migrateOwner ? { migrateOwner: true } : {}), + version, + changelog, + ...(clawScanNote ? { clawScanNote } : {}), + acceptLicenseTerms: true, + tags, + ...(forkOf ? { forkOf } : {}), + }) + ); + + let index = 0; + for (const file of filesOnDisk) { + index += 1; + spinner.text = `Uploading ${file.relPath} (${index}/${filesOnDisk.length})`; + const blob = new Blob([Buffer.from(file.bytes)], { + type: file.contentType ?? 'text/plain', + }); + form.append('files', blob, file.relPath); + } + + spinner.text = `Publishing ${slug}@${version}`; + const result = await apiRequestForm( + registry, + { method: 'POST', path: ApiRoutes.skills, form }, + ApiV1PublishResponseSchema + ); + + spinner.succeed(`OK. Published ${slug}@${version} (${result.versionId})`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } async function ensureRootManifestFile( - folder: string, - files: Awaited>, + folder: string, + files: Awaited> ) { - if ( - files.some((file) => { - const lower = file.relPath.toLowerCase(); - return lower === "skill.md" || lower === "skills.md"; - }) - ) { - return files; - } - - const entries = await readdir(folder, { withFileTypes: true }).catch(() => []); - const manifest = entries.find((entry) => { - const lower = entry.name.toLowerCase(); - return entry.isFile() && (lower === "skill.md" || lower === "skills.md"); - }); - if (!manifest) return files; - - return [ - ...files, - { - relPath: manifest.name, - bytes: new Uint8Array(await readFile(join(folder, manifest.name))), - contentType: "text/markdown", - }, - ]; + if ( + files.some((file) => { + const lower = file.relPath.toLowerCase(); + return lower === 'skill.md' || lower === 'skills.md'; + }) + ) { + return files; + } + + const entries = await readdir(folder, { withFileTypes: true }).catch(() => []); + const manifest = entries.find((entry) => { + const lower = entry.name.toLowerCase(); + return entry.isFile() && (lower === 'skill.md' || lower === 'skills.md'); + }); + if (!manifest) return files; + + return [ + ...files, + { + relPath: manifest.name, + bytes: new Uint8Array(await readFile(join(folder, manifest.name))), + contentType: 'text/markdown', + }, + ]; } async function looksLikePluginFolder(folder: string) { - const checks = [ - join(folder, "openclaw.plugin.json"), - join(folder, "package.json"), - join(folder, ".codex-plugin", "plugin.json"), - join(folder, ".claude-plugin", "plugin.json"), - join(folder, ".cursor-plugin", "plugin.json"), - ]; - const stats = await Promise.all(checks.map((candidate) => stat(candidate).catch(() => null))); - if (stats[0]?.isFile() || stats[2]?.isFile() || stats[3]?.isFile() || stats[4]?.isFile()) { - return true; - } - if (!stats[1]?.isFile()) { - return false; - } - try { - const raw = JSON.parse(await readFile(checks[1], "utf8")) as { openclaw?: unknown }; - return Boolean( - raw && typeof raw === "object" && raw.openclaw && typeof raw.openclaw === "object", - ); - } catch { - return false; - } + const checks = [ + join(folder, 'openclaw.plugin.json'), + join(folder, 'package.json'), + join(folder, '.codex-plugin', 'plugin.json'), + join(folder, '.claude-plugin', 'plugin.json'), + join(folder, '.cursor-plugin', 'plugin.json'), + ]; + const stats = await Promise.all(checks.map((candidate) => stat(candidate).catch(() => null))); + if (stats[0]?.isFile() || stats[2]?.isFile() || stats[3]?.isFile() || stats[4]?.isFile()) { + return true; + } + if (!stats[1]?.isFile()) { + return false; + } + try { + const raw = JSON.parse(await readFile(checks[1], 'utf8')) as { openclaw?: unknown }; + return Boolean( + raw && typeof raw === 'object' && raw.openclaw && typeof raw.openclaw === 'object' + ); + } catch { + return false; + } } function parseForkOf(value: string) { - const trimmed = value.trim(); - const [slugRaw, versionRaw] = trimmed.split("@"); - const slug = (slugRaw ?? "").trim().toLowerCase(); - if (!slug) fail("--fork-of must be or "); - const version = (versionRaw ?? "").trim(); - if (version && !semver.valid(version)) fail("--fork-of version must be valid semver"); - return { slug, version: version || undefined }; + const trimmed = value.trim(); + const [slugRaw, versionRaw] = trimmed.split('@'); + const slug = (slugRaw ?? '').trim().toLowerCase(); + if (!slug) fail('--fork-of must be or '); + const version = (versionRaw ?? '').trim(); + if (version && !semver.valid(version)) fail('--fork-of version must be valid semver'); + return { slug, version: version || undefined }; } export async function cmdPublishBatch( - opts: GlobalOpts, - folder: string, - discoveredSkills: Array<{ folder: string; slug: string; displayName: string }>, - options: { - all?: boolean; - category?: string; - tags?: string; - name?: string; - }, + opts: GlobalOpts, + folder: string, + discoveredSkills: Array<{ folder: string; slug: string; displayName: string }>, + options: { + all?: boolean; + category?: string; + tags?: string; + name?: string; + } ) { - const registry = await getRegistry(opts, { cache: true }); - - let selectedSkills = discoveredSkills; - - // Interactive selection when not --all and terminal supports interaction - if (!options.all && isInteractive()) { - const items = discoveredSkills.map((s) => ({ - value: s.slug, - label: s.displayName, - hint: s.folder.split("/").pop() ?? "", - })); - const selected = await searchMultiselect({ - message: `Select skills to publish (${discoveredSkills.length} found):`, - items, - required: true, - }); - - if (typeof selected === "symbol") { - console.log("Upload cancelled"); - return; + const registry = await getRegistry(opts, { cache: true }); + + let selectedSkills = discoveredSkills; + + // Interactive selection when not --all and terminal supports interaction + if (!options.all && isInteractive()) { + const items = discoveredSkills.map((s) => ({ + value: s.slug, + label: s.displayName, + hint: s.folder.split('/').pop() ?? '', + })); + const selected = await searchMultiselect({ + message: `Select skills to publish (${discoveredSkills.length} found):`, + items, + required: true, + }); + + if (typeof selected === 'symbol') { + console.log('Upload cancelled'); + return; + } + + const selectedSlugs = selected as string[]; + selectedSkills = discoveredSkills.filter((s) => selectedSlugs.includes(s.slug)); + + if (selectedSkills.length === 0) { + console.log('No skills selected'); + return; + } } - const selectedSlugs = selected as string[]; - selectedSkills = discoveredSkills.filter((s) => selectedSlugs.includes(s.slug)); + // Derive package name from folder basename (e.g. "demo-multi-skill-folders") + const packageBaseName = options.name?.trim() || basename(folder); + const zipFileName = `${packageBaseName}.zip`; - if (selectedSkills.length === 0) { - console.log("No skills selected"); - return; - } - } - - // Derive package name from folder basename (e.g. "demo-multi-skill-folders") - const packageBaseName = options.name?.trim() || basename(folder); - const zipFileName = `${packageBaseName}.zip`; - - // Pack selected skills into a ZIP - const spinner = createSpinner( - `Packing ${selectedSkills.length} skill(s) into ZIP`, - ); - const zip = new AdmZip(); - - for (const skill of selectedSkills) { - zip.addLocalFolder(skill.folder, skill.slug); - } - - const zipBuffer = zip.toBuffer(); - spinner.text = `Uploading ${selectedSkills.length} skill(s) as ${zipFileName}`; - - // Upload via /api/skills/import-file - try { - const form = new FormData(); - const blob = new Blob([zipBuffer], { type: "application/zip" }); - form.set("file", blob, zipFileName); - if (packageBaseName) form.set("packageName", packageBaseName); - if (options.category) form.set("category", options.category); - if (options.tags) form.set("tags", options.tags); - - const result = await apiRequestForm<{ - success: boolean; - data: { - importedCount: number; - refreshedCount: number; - importedSkills: Array<{ slug: string; name: string }>; - }; - }>(registry, { - method: "POST", - path: "/api/skills/import-file", - form, - }); + // Pack selected skills into a ZIP + const spinner = createSpinner(`Packing ${selectedSkills.length} skill(s) into ZIP`); + const zip = new AdmZip(); - const data = result.data; - spinner.succeed( - `✓ Uploaded ${data.importedCount} skill(s)` + - (data.importedCount > 1 ? ` (package created)` : ""), - ); + for (const skill of selectedSkills) { + zip.addLocalFolder(skill.folder, skill.slug); + } - for (const skill of data.importedSkills) { - console.log(` - ${skill.name} (${skill.slug})`); + const zipBuffer = zip.toBuffer(); + spinner.text = `Uploading ${selectedSkills.length} skill(s) as ${zipFileName}`; + + // Upload via /api/skills/import-file + try { + const form = new FormData(); + const blob = new Blob([zipBuffer], { type: 'application/zip' }); + form.set('file', blob, zipFileName); + if (packageBaseName) form.set('packageName', packageBaseName); + if (options.category) form.set('category', options.category); + if (options.tags) form.set('tags', options.tags); + + const result = await apiRequestForm<{ + success: boolean; + data: { + importedCount: number; + refreshedCount: number; + importedSkills: Array<{ slug: string; name: string }>; + }; + }>(registry, { + method: 'POST', + path: '/api/skills/import-file', + form, + }); + + const data = result.data; + spinner.succeed( + `✓ Uploaded ${data.importedCount} skill(s)` + + (data.importedCount > 1 ? ` (package created)` : '') + ); + + for (const skill of data.importedSkills) { + console.log(` - ${skill.name} (${skill.slug})`); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; } - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } } /** @@ -302,14 +300,14 @@ export async function cmdPublishBatch( * workdir points to a clawdbot workspace or cwd. */ async function resolveFolderPath(workdir: string, folderArg: string): Promise { - const fromWorkdir = resolve(workdir, folderArg); - const workdirStat = await stat(fromWorkdir).catch(() => null); - if (workdirStat?.isDirectory()) return fromWorkdir; + const fromWorkdir = resolve(workdir, folderArg); + const workdirStat = await stat(fromWorkdir).catch(() => null); + if (workdirStat?.isDirectory()) return fromWorkdir; - const fromCwd = resolve(process.cwd(), folderArg); - const cwdStat = await stat(fromCwd).catch(() => null); - if (cwdStat?.isDirectory()) return fromCwd; + const fromCwd = resolve(process.cwd(), folderArg); + const cwdStat = await stat(fromCwd).catch(() => null); + if (cwdStat?.isDirectory()) return fromCwd; - // Return the workdir-relative path so the original "Path must be a folder" error fires - return fromWorkdir; + // Return the workdir-relative path so the original "Path must be a folder" error fires + return fromWorkdir; } diff --git a/dt-skill/src/cli/commands/skills.install.test.ts b/dt-skill/src/cli/commands/skills.install.test.ts index a078b843..53937c49 100644 --- a/dt-skill/src/cli/commands/skills.install.test.ts +++ b/dt-skill/src/cli/commands/skills.install.test.ts @@ -1,28 +1,29 @@ /* @vitest-environment node */ -import * as fsPromises from "node:fs/promises"; -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; -import * as skillStore from "../../skills.js"; +import * as fsPromises from 'node:fs/promises'; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from '../../../test/cliCommandTestKit.js'; +import * as skillStore from '../../skills.js'; const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn(), - rm: vi.fn(), - stat: vi.fn(), + mkdir: vi.fn(), + rm: vi.fn(), + stat: vi.fn(), })); -vi.mock("node:fs/promises", async () => { - const actual = await vi.importActual("node:fs/promises"); - return { - ...actual, - mkdir: fsMocks.mkdir, - rm: fsMocks.rm, - stat: fsMocks.stat, - }; +vi.mock('node:fs/promises', async () => { + const actual = await vi.importActual('node:fs/promises'); + return { + ...actual, + mkdir: fsMocks.mkdir, + rm: fsMocks.rm, + stat: fsMocks.stat, + }; }); const mocked = (value: T) => value as T & Record; @@ -41,310 +42,308 @@ const mockSelectScope = vi.fn(async () => false); const mockSearchMultiselect = vi.fn(); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); -vi.mock("../ui.js", () => ({ - createSpinner: vi.fn(() => mockSpinner), - fail: (message: string) => uiMocks.fail(message), - formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), - isInteractive: mockIsInteractive, - promptConfirm: mockPromptConfirm, - selectAgent: mockSelectAgent, - selectScope: mockSelectScope, +vi.mock('../../http.js', () => httpMocks.moduleFactory()); +vi.mock('../registry.js', () => registryMocks.moduleFactory()); +vi.mock('../ui.js', () => ({ + createSpinner: vi.fn(() => mockSpinner), + fail: (message: string) => uiMocks.fail(message), + formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), + isInteractive: mockIsInteractive, + promptConfirm: mockPromptConfirm, + selectAgent: mockSelectAgent, + selectScope: mockSelectScope, })); -vi.mock("../prompts/search-multiselect.js", async () => { - const actual = await vi.importActual("../prompts/search-multiselect.js"); - return { - ...actual, - searchMultiselect: (opts: any) => mockSearchMultiselect(opts), - }; +vi.mock('../prompts/search-multiselect.js', async () => { + const actual = await vi.importActual('../prompts/search-multiselect.js'); + return { + ...actual, + searchMultiselect: (opts: any) => mockSearchMultiselect(opts), + }; }); -const extractZipToDirMock = vi.spyOn(skillStore, "extractZipToDir"); -const hashSkillFilesMock = vi.spyOn(skillStore, "hashSkillFiles"); -const listTextFilesMock = vi.spyOn(skillStore, "listTextFiles"); -const readLockfileMock = vi.spyOn(skillStore, "readLockfile"); -const readSkillOriginMock = vi.spyOn(skillStore, "readSkillOrigin"); -const writeLockfileMock = vi.spyOn(skillStore, "writeLockfile"); -const writeSkillOriginMock = vi.spyOn(skillStore, "writeSkillOrigin"); +const extractZipToDirMock = vi.spyOn(skillStore, 'extractZipToDir'); +const hashSkillFilesMock = vi.spyOn(skillStore, 'hashSkillFiles'); +const listTextFilesMock = vi.spyOn(skillStore, 'listTextFiles'); +const readLockfileMock = vi.spyOn(skillStore, 'readLockfile'); +const readSkillOriginMock = vi.spyOn(skillStore, 'readSkillOrigin'); +const writeLockfileMock = vi.spyOn(skillStore, 'writeLockfile'); +const writeSkillOriginMock = vi.spyOn(skillStore, 'writeSkillOrigin'); const mkdirMock = fsMocks.mkdir; const rmMock = fsMocks.rm; const statMock = fsMocks.stat; -const { cmdInstall } = await import("./skills.js"); +const { cmdInstall } = await import('./skills.js'); -const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); +const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {}); function makeOpts() { - return makeGlobalOpts(); + return makeGlobalOpts(); } beforeEach(() => { - process.exitCode = undefined; - mkdirMock.mockResolvedValue(undefined); - rmMock.mockResolvedValue(undefined); - statMock.mockRejectedValue(new Error("missing")); - extractZipToDirMock.mockResolvedValue(undefined); - hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: [] }); - listTextFilesMock.mockResolvedValue([]); - readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); - readSkillOriginMock.mockResolvedValue(null); - writeLockfileMock.mockResolvedValue(undefined); - writeSkillOriginMock.mockResolvedValue(undefined); + process.exitCode = undefined; + mkdirMock.mockResolvedValue(undefined); + rmMock.mockResolvedValue(undefined); + statMock.mockRejectedValue(new Error('missing')); + extractZipToDirMock.mockResolvedValue(undefined); + hashSkillFilesMock.mockReturnValue({ fingerprint: 'hash', files: [] }); + listTextFilesMock.mockResolvedValue([]); + readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); + readSkillOriginMock.mockResolvedValue(null); + writeLockfileMock.mockResolvedValue(undefined); + writeSkillOriginMock.mockResolvedValue(undefined); }); afterEach(() => { - vi.clearAllMocks(); + vi.clearAllMocks(); }); afterAll(() => { - extractZipToDirMock.mockRestore(); - hashSkillFilesMock.mockRestore(); - listTextFilesMock.mockRestore(); - readLockfileMock.mockRestore(); - readSkillOriginMock.mockRestore(); - writeLockfileMock.mockRestore(); - writeSkillOriginMock.mockRestore(); + extractZipToDirMock.mockRestore(); + hashSkillFilesMock.mockRestore(); + listTextFilesMock.mockRestore(); + readLockfileMock.mockRestore(); + readSkillOriginMock.mockRestore(); + writeLockfileMock.mockRestore(); + writeSkillOriginMock.mockRestore(); }); -describe("cmdInstall with packages", () => { - it("installs single non-package skill directly", async () => { - mockApiRequest.mockResolvedValue({ - skill: { - slug: "single-skill", - displayName: "Single Skill", - isPackage: 0, - }, - latestVersion: { version: "1.0.0" }, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - - await cmdInstall(makeOpts(), "single-skill"); - - expect(mockApiRequest).toHaveBeenCalledWith( - "https://example.com", - expect.objectContaining({ path: "/api/v1/skills/single-skill" }), - expect.anything() - ); - expect(mockDownloadZip).toHaveBeenCalledWith( - "https://example.com", - { slug: "single-skill", version: "1.0.0" } - ); - expect(extractZipToDirMock).toHaveBeenCalled(); - expect(writeLockfileMock).toHaveBeenCalled(); - }); - - it("installs multiple skills in batch", async () => { - mockApiRequest.mockImplementation(async (_registry, request) => { - const slug = (request as any).path?.split("/").pop(); - return { - skill: { slug, displayName: slug, isPackage: 0 }, - latestVersion: { version: "1.0.0" }, - }; - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - - await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); - - expect(mockApiRequest).toHaveBeenCalledTimes(3); - expect(mockDownloadZip).toHaveBeenCalledTimes(3); - expect(extractZipToDirMock).toHaveBeenCalledTimes(3); - expect(writeLockfileMock).toHaveBeenCalledTimes(3); - }); - - it("installs skill when latestVersion is null", async () => { - mockApiRequest.mockResolvedValue({ - skill: { - slug: "no-version-skill", - displayName: "No Version Skill", - isPackage: 0, - }, - latestVersion: null, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - - await cmdInstall(makeOpts(), "no-version-skill"); - - expect(mockApiRequest).toHaveBeenCalledWith( - "https://example.com", - expect.objectContaining({ path: "/api/v1/skills/no-version-skill" }), - expect.anything() - ); - expect(mockDownloadZip).toHaveBeenCalledWith( - "https://example.com", - { slug: "no-version-skill", version: "latest" } - ); - expect(extractZipToDirMock).toHaveBeenCalled(); - expect(writeLockfileMock).toHaveBeenCalled(); - }); - - it("continues batch install when one skill fails", async () => { - let callCount = 0; - mockApiRequest.mockImplementation(async (_registry, request) => { - callCount++; - const slug = (request as any).path?.split("/").pop(); - if (slug === "skill-b") { - throw new Error("not found"); - } - return { - skill: { slug, displayName: slug, isPackage: 0 }, - latestVersion: { version: "1.0.0" }, - }; - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - - await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); - - expect(mockApiRequest).toHaveBeenCalledTimes(3); - expect(mockDownloadZip).toHaveBeenCalledTimes(2); - expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Summary")); - expect(process.exitCode).toBe(1); - }); - - it("continues batch install when fail() is triggered for one skill", async () => { - mockApiRequest.mockImplementation(async (_registry, request) => { - const slug = (request as any).path?.split("/").pop(); - return { - skill: { slug, displayName: slug, isPackage: 0 }, - latestVersion: { version: "1.0.0" }, - }; - }); - statMock.mockImplementation(async (path: string) => { - if (path.includes("skill-b")) { - return { isFile: () => true, isDirectory: () => false } as any; - } - throw new Error("missing"); - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - - await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); - - expect(mockApiRequest).toHaveBeenCalledTimes(3); - expect(mockDownloadZip).toHaveBeenCalledTimes(2); - expect(mockSpinner.fail).toHaveBeenCalledWith(expect.stringContaining("Already installed")); - expect(mockLog).toHaveBeenCalledWith(expect.stringContaining("Summary")); - }); - - it("fails if package has no children skills", async () => { - mockApiRequest.mockResolvedValue({ - skill: { - slug: "empty-package", - displayName: "Empty Package", - isPackage: 1, - children: [], - }, - latestVersion: { version: "1.0.0" }, +describe('cmdInstall with packages', () => { + it('installs single non-package skill directly', async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'single-skill', + displayName: 'Single Skill', + isPackage: 0, + }, + latestVersion: { version: '1.0.0' }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), 'single-skill'); + + expect(mockApiRequest).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ path: '/api/v1/skills/single-skill' }), + expect.anything() + ); + expect(mockDownloadZip).toHaveBeenCalledWith('https://example.com', { + slug: 'single-skill', + version: '1.0.0', + }); + expect(extractZipToDirMock).toHaveBeenCalled(); + expect(writeLockfileMock).toHaveBeenCalled(); }); - await expect(cmdInstall(makeOpts(), "empty-package")).rejects.toThrow( - 'Skill package "empty-package" has no children skills.' - ); - }); - - it("installs selected sub-skills from package using searchMultiselect", async () => { - mockApiRequest.mockResolvedValue({ - skill: { - slug: "my-package", - displayName: "My Package", - isPackage: 1, - children: [ - { slug: "sub-1", displayName: "Sub 1", version: "1.1.0", summary: "Hint 1" }, - { slug: "sub-2", displayName: "Sub 2", version: "1.2.0", summary: "Hint 2" }, - ], - }, - latestVersion: { version: "1.0.0" }, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - mockSearchMultiselect.mockResolvedValue(["sub-2"]); - - await cmdInstall(makeOpts(), "my-package"); - - expect(mockSearchMultiselect).toHaveBeenCalledWith({ - message: 'Select skills from package "my-package" to install:', - items: [ - { value: "sub-1", label: "Sub 1", hint: "Hint 1" }, - { value: "sub-2", label: "Sub 2", hint: "Hint 2" }, - ], - required: true, + it('installs multiple skills in batch', async () => { + mockApiRequest.mockImplementation(async (_registry, request) => { + const slug = (request as any).path?.split('/').pop(); + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: '1.0.0' }, + }; + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ['skill-a', 'skill-b', 'skill-c']); + + expect(mockApiRequest).toHaveBeenCalledTimes(3); + expect(mockDownloadZip).toHaveBeenCalledTimes(3); + expect(extractZipToDirMock).toHaveBeenCalledTimes(3); + expect(writeLockfileMock).toHaveBeenCalledTimes(3); }); - expect(mockDownloadZip).toHaveBeenCalledTimes(1); - expect(mockDownloadZip).toHaveBeenCalledWith( - "https://example.com", - { slug: "sub-2", version: "1.2.0" } - ); - - expect(extractZipToDirMock).toHaveBeenCalledTimes(1); - expect(writeSkillOriginMock).toHaveBeenCalledTimes(1); - expect(writeLockfileMock).toHaveBeenCalledTimes(1); - }); - - it("installs all sub-skills when user selects all from package", async () => { - mockApiRequest.mockResolvedValue({ - skill: { - slug: "full-package", - displayName: "Full Package", - isPackage: 1, - children: [ - { slug: "child-a", displayName: "Child A", version: "1.0.0" }, - { slug: "child-b", displayName: "Child B", version: "2.0.0" }, - { slug: "child-c", displayName: "Child C", version: "3.0.0" }, - ], - }, - latestVersion: { version: "1.0.0" }, + it('installs skill when latestVersion is null', async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'no-version-skill', + displayName: 'No Version Skill', + isPackage: 0, + }, + latestVersion: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), 'no-version-skill'); + + expect(mockApiRequest).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ path: '/api/v1/skills/no-version-skill' }), + expect.anything() + ); + expect(mockDownloadZip).toHaveBeenCalledWith('https://example.com', { + slug: 'no-version-skill', + version: 'latest', + }); + expect(extractZipToDirMock).toHaveBeenCalled(); + expect(writeLockfileMock).toHaveBeenCalled(); }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - mockSearchMultiselect.mockResolvedValue(["child-a", "child-b", "child-c"]); - - await cmdInstall(makeOpts(), "full-package"); - - expect(mockDownloadZip).toHaveBeenCalledTimes(3); - expect(extractZipToDirMock).toHaveBeenCalledTimes(3); - expect(writeLockfileMock).toHaveBeenCalledTimes(3); - }); - - it("handles user cancellation in searchMultiselect", async () => { - const { cancelSymbol: realCancelSymbol } = await import("../prompts/search-multiselect.js"); - mockApiRequest.mockResolvedValue({ - skill: { - slug: "my-package", - displayName: "My Package", - isPackage: 1, - children: [ - { slug: "sub-1", displayName: "Sub 1" }, - ], - }, - latestVersion: { version: "1.0.0" }, + + it('continues batch install when one skill fails', async () => { + let callCount = 0; + mockApiRequest.mockImplementation(async (_registry, request) => { + callCount++; + const slug = (request as any).path?.split('/').pop(); + if (slug === 'skill-b') { + throw new Error('not found'); + } + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: '1.0.0' }, + }; + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ['skill-a', 'skill-b', 'skill-c']); + + expect(mockApiRequest).toHaveBeenCalledTimes(3); + expect(mockDownloadZip).toHaveBeenCalledTimes(2); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining('Summary')); + expect(process.exitCode).toBe(1); }); - mockSearchMultiselect.mockResolvedValue(realCancelSymbol); - await cmdInstall(makeOpts(), "my-package"); + it('continues batch install when fail() is triggered for one skill', async () => { + mockApiRequest.mockImplementation(async (_registry, request) => { + const slug = (request as any).path?.split('/').pop(); + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: '1.0.0' }, + }; + }); + statMock.mockImplementation(async (path: string) => { + if (path.includes('skill-b')) { + return { isFile: () => true, isDirectory: () => false } as any; + } + throw new Error('missing'); + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ['skill-a', 'skill-b', 'skill-c']); + + expect(mockApiRequest).toHaveBeenCalledTimes(3); + expect(mockDownloadZip).toHaveBeenCalledTimes(2); + expect(mockSpinner.fail).toHaveBeenCalledWith(expect.stringContaining('Already installed')); + expect(mockLog).toHaveBeenCalledWith(expect.stringContaining('Summary')); + }); - expect(mockLog).toHaveBeenCalledWith("Installation cancelled"); - expect(mockDownloadZip).not.toHaveBeenCalled(); - }); + it('fails if package has no children skills', async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'empty-package', + displayName: 'Empty Package', + isPackage: 1, + children: [], + }, + latestVersion: { version: '1.0.0' }, + }); + + await expect(cmdInstall(makeOpts(), 'empty-package')).rejects.toThrow( + 'Skill package "empty-package" has no children skills.' + ); + }); - it("prompts agent only once during batch install", async () => { - mockIsInteractive.mockReturnValue(true); - mockSelectAgent.mockResolvedValue({ - agent: "claude-code", - workdir: "/mock/.claude", - dir: "/mock/.claude/skills", + it('installs selected sub-skills from package using searchMultiselect', async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'my-package', + displayName: 'My Package', + isPackage: 1, + children: [ + { slug: 'sub-1', displayName: 'Sub 1', version: '1.1.0', summary: 'Hint 1' }, + { slug: 'sub-2', displayName: 'Sub 2', version: '1.2.0', summary: 'Hint 2' }, + ], + }, + latestVersion: { version: '1.0.0' }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + mockSearchMultiselect.mockResolvedValue(['sub-2']); + + await cmdInstall(makeOpts(), 'my-package'); + + expect(mockSearchMultiselect).toHaveBeenCalledWith({ + message: 'Select skills from package "my-package" to install:', + items: [ + { value: 'sub-1', label: 'Sub 1', hint: 'Hint 1' }, + { value: 'sub-2', label: 'Sub 2', hint: 'Hint 2' }, + ], + required: true, + }); + + expect(mockDownloadZip).toHaveBeenCalledTimes(1); + expect(mockDownloadZip).toHaveBeenCalledWith('https://example.com', { + slug: 'sub-2', + version: '1.2.0', + }); + + expect(extractZipToDirMock).toHaveBeenCalledTimes(1); + expect(writeSkillOriginMock).toHaveBeenCalledTimes(1); + expect(writeLockfileMock).toHaveBeenCalledTimes(1); }); - mockApiRequest.mockImplementation(async (_registry, request) => { - const slug = (request as any).path?.split("/").pop(); - return { - skill: { slug, displayName: slug, isPackage: 0 }, - latestVersion: { version: "1.0.0" }, - }; + + it('installs all sub-skills when user selects all from package', async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'full-package', + displayName: 'Full Package', + isPackage: 1, + children: [ + { slug: 'child-a', displayName: 'Child A', version: '1.0.0' }, + { slug: 'child-b', displayName: 'Child B', version: '2.0.0' }, + { slug: 'child-c', displayName: 'Child C', version: '3.0.0' }, + ], + }, + latestVersion: { version: '1.0.0' }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + mockSearchMultiselect.mockResolvedValue(['child-a', 'child-b', 'child-c']); + + await cmdInstall(makeOpts(), 'full-package'); + + expect(mockDownloadZip).toHaveBeenCalledTimes(3); + expect(extractZipToDirMock).toHaveBeenCalledTimes(3); + expect(writeLockfileMock).toHaveBeenCalledTimes(3); }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - await cmdInstall(makeOpts(), ["skill-a", "skill-b", "skill-c"]); + it('handles user cancellation in searchMultiselect', async () => { + const { cancelSymbol: realCancelSymbol } = await import('../prompts/search-multiselect.js'); + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'my-package', + displayName: 'My Package', + isPackage: 1, + children: [{ slug: 'sub-1', displayName: 'Sub 1' }], + }, + latestVersion: { version: '1.0.0' }, + }); + mockSearchMultiselect.mockResolvedValue(realCancelSymbol); + + await cmdInstall(makeOpts(), 'my-package'); + + expect(mockLog).toHaveBeenCalledWith('Installation cancelled'); + expect(mockDownloadZip).not.toHaveBeenCalled(); + }); - expect(mockSelectAgent).toHaveBeenCalledTimes(1); - expect(mkdirMock).toHaveBeenCalledWith("/mock/.claude/skills", { recursive: true }); - expect(mockDownloadZip).toHaveBeenCalledTimes(3); - }); + it('prompts agent only once during batch install', async () => { + mockIsInteractive.mockReturnValue(true); + mockSelectAgent.mockResolvedValue({ + agent: 'claude-code', + workdir: '/mock/.claude', + dir: '/mock/.claude/skills', + }); + mockApiRequest.mockImplementation(async (_registry, request) => { + const slug = (request as any).path?.split('/').pop(); + return { + skill: { slug, displayName: slug, isPackage: 0 }, + latestVersion: { version: '1.0.0' }, + }; + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + + await cmdInstall(makeOpts(), ['skill-a', 'skill-b', 'skill-c']); + + expect(mockSelectAgent).toHaveBeenCalledTimes(1); + expect(mkdirMock).toHaveBeenCalledWith('/mock/.claude/skills', { recursive: true }); + expect(mockDownloadZip).toHaveBeenCalledTimes(3); + }); }); diff --git a/dt-skill/src/cli/commands/skills.test.ts b/dt-skill/src/cli/commands/skills.test.ts index 09befbc9..8a42d54f 100644 --- a/dt-skill/src/cli/commands/skills.test.ts +++ b/dt-skill/src/cli/commands/skills.test.ts @@ -1,33 +1,34 @@ /* @vitest-environment node */ -import * as fsPromises from "node:fs/promises"; -import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createHttpModuleMocks, - createRegistryModuleMocks, - createUiModuleMocks, - makeGlobalOpts, -} from "../../../test/cliCommandTestKit.js"; -import { ApiRoutes } from "../../schema/index.js"; -import * as skillStore from "../../skills.js"; +import * as fsPromises from 'node:fs/promises'; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + createHttpModuleMocks, + createRegistryModuleMocks, + createUiModuleMocks, + makeGlobalOpts, +} from '../../../test/cliCommandTestKit.js'; +import { ApiRoutes } from '../../schema/index.js'; +import * as skillStore from '../../skills.js'; const fsMocks = vi.hoisted(() => ({ - mkdir: vi.fn(), - mkdtemp: vi.fn(), - rename: vi.fn(), - rm: vi.fn(), - stat: vi.fn(), + mkdir: vi.fn(), + mkdtemp: vi.fn(), + rename: vi.fn(), + rm: vi.fn(), + stat: vi.fn(), })); -vi.mock("node:fs/promises", async () => { - const actual = await vi.importActual("node:fs/promises"); - return { - ...actual, - mkdir: fsMocks.mkdir, - mkdtemp: fsMocks.mkdtemp, - rename: fsMocks.rename, - rm: fsMocks.rm, - stat: fsMocks.stat, - }; +vi.mock('node:fs/promises', async () => { + const actual = await vi.importActual('node:fs/promises'); + return { + ...actual, + mkdir: fsMocks.mkdir, + mkdtemp: fsMocks.mkdtemp, + rename: fsMocks.rename, + rm: fsMocks.rm, + stat: fsMocks.stat, + }; }); const mocked = (value: T) => value as T & Record; @@ -41,25 +42,25 @@ const mockDownloadZip = httpMocks.downloadZip; const mockSpinner = uiMocks.spinner; const mockIsInteractive = vi.fn(() => false); const mockPromptConfirm = vi.fn(async () => false); -vi.mock("../../http.js", () => httpMocks.moduleFactory()); -vi.mock("../registry.js", () => registryMocks.moduleFactory()); +vi.mock('../../http.js', () => httpMocks.moduleFactory()); +vi.mock('../registry.js', () => registryMocks.moduleFactory()); const mockSelectAgent = vi.fn(async () => null); -vi.mock("../ui.js", () => ({ - createSpinner: vi.fn(() => mockSpinner), - fail: (message: string) => uiMocks.fail(message), - formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), - isInteractive: mockIsInteractive, - promptConfirm: mockPromptConfirm, - selectAgent: mockSelectAgent, +vi.mock('../ui.js', () => ({ + createSpinner: vi.fn(() => mockSpinner), + fail: (message: string) => uiMocks.fail(message), + formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), + isInteractive: mockIsInteractive, + promptConfirm: mockPromptConfirm, + selectAgent: mockSelectAgent, })); -const extractZipToDirMock = vi.spyOn(skillStore, "extractZipToDir"); -const hashSkillFilesMock = vi.spyOn(skillStore, "hashSkillFiles"); -const listTextFilesMock = vi.spyOn(skillStore, "listTextFiles"); -const readLockfileMock = vi.spyOn(skillStore, "readLockfile"); -const readSkillOriginMock = vi.spyOn(skillStore, "readSkillOrigin"); -const writeLockfileMock = vi.spyOn(skillStore, "writeLockfile"); -const writeSkillOriginMock = vi.spyOn(skillStore, "writeSkillOrigin"); +const extractZipToDirMock = vi.spyOn(skillStore, 'extractZipToDir'); +const hashSkillFilesMock = vi.spyOn(skillStore, 'hashSkillFiles'); +const listTextFilesMock = vi.spyOn(skillStore, 'listTextFiles'); +const readLockfileMock = vi.spyOn(skillStore, 'readLockfile'); +const readSkillOriginMock = vi.spyOn(skillStore, 'readSkillOrigin'); +const writeLockfileMock = vi.spyOn(skillStore, 'writeLockfile'); +const writeSkillOriginMock = vi.spyOn(skillStore, 'writeSkillOrigin'); const mkdirMock = fsMocks.mkdir; const mkdtempMock = fsMocks.mkdtemp; @@ -67,810 +68,834 @@ const renameMock = fsMocks.rename; const rmMock = fsMocks.rm; const statMock = fsMocks.stat; const { - clampLimit, - cmdExplore, - cmdInstall, - cmdList, cmdPin, cmdSearch, cmdUninstall, - cmdUnpin, - cmdUpdate, - formatExploreLine, -} = await import("./skills.js"); + clampLimit, + cmdExplore, + cmdInstall, + cmdList, + cmdPin, + cmdSearch, + cmdUninstall, + cmdUnpin, + cmdUpdate, + formatExploreLine, +} = await import('./skills.js'); const { - extractZipToDir, - hashSkillFiles, - listTextFiles, - readLockfile, - readSkillOrigin, - writeLockfile, - writeSkillOrigin, + extractZipToDir, + hashSkillFiles, + listTextFiles, + readLockfile, + readSkillOrigin, + writeLockfile, + writeSkillOrigin, } = skillStore; const { rm, stat } = fsPromises; -const mockLog = vi.spyOn(console, "log").mockImplementation(() => {}); +const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {}); function makeOpts() { - return makeGlobalOpts(); + return makeGlobalOpts(); } beforeEach(() => { - mkdirMock.mockResolvedValue(undefined); - mkdtempMock.mockResolvedValue("/work/skills/.demo-update-test"); - renameMock.mockResolvedValue(undefined); - rmMock.mockResolvedValue(undefined); - statMock.mockRejectedValue(new Error("missing")); - extractZipToDirMock.mockResolvedValue(undefined); - hashSkillFilesMock.mockReturnValue({ fingerprint: "hash", files: [] }); - listTextFilesMock.mockResolvedValue([]); - readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); - readSkillOriginMock.mockResolvedValue(null); - writeLockfileMock.mockResolvedValue(undefined); - writeSkillOriginMock.mockResolvedValue(undefined); + mkdirMock.mockResolvedValue(undefined); + mkdtempMock.mockResolvedValue('/work/skills/.demo-update-test'); + renameMock.mockResolvedValue(undefined); + rmMock.mockResolvedValue(undefined); + statMock.mockRejectedValue(new Error('missing')); + extractZipToDirMock.mockResolvedValue(undefined); + hashSkillFilesMock.mockReturnValue({ fingerprint: 'hash', files: [] }); + listTextFilesMock.mockResolvedValue([]); + readLockfileMock.mockResolvedValue({ version: 1, skills: {} }); + readSkillOriginMock.mockResolvedValue(null); + writeLockfileMock.mockResolvedValue(undefined); + writeSkillOriginMock.mockResolvedValue(undefined); }); afterEach(() => { - vi.clearAllMocks(); + vi.clearAllMocks(); }); afterAll(() => { - extractZipToDirMock.mockRestore(); - hashSkillFilesMock.mockRestore(); - listTextFilesMock.mockRestore(); - readLockfileMock.mockRestore(); - readSkillOriginMock.mockRestore(); - writeLockfileMock.mockRestore(); - writeSkillOriginMock.mockRestore(); + extractZipToDirMock.mockRestore(); + hashSkillFilesMock.mockRestore(); + listTextFilesMock.mockRestore(); + readLockfileMock.mockRestore(); + readSkillOriginMock.mockRestore(); + writeLockfileMock.mockRestore(); + writeSkillOriginMock.mockRestore(); }); -describe("explore helpers", () => { - it("clamps explore limits and handles non-finite values", () => { - expect(clampLimit(-5)).toBe(1); - expect(clampLimit(0)).toBe(1); - expect(clampLimit(1)).toBe(1); - expect(clampLimit(50)).toBe(50); - expect(clampLimit(99)).toBe(99); - expect(clampLimit(200)).toBe(200); - expect(clampLimit(250)).toBe(200); - expect(clampLimit(Number.NaN)).toBe(25); - expect(clampLimit(Number.POSITIVE_INFINITY)).toBe(25); - expect(clampLimit(Number.NaN, 10)).toBe(10); - }); - - it("formats explore lines with relative time and truncation", () => { - const now = 4 * 60 * 60 * 1000; - const nowSpy = vi.spyOn(Date, "now").mockReturnValue(now); - const summary = "a".repeat(60); - const line = formatExploreLine({ - slug: "weather", - summary, - updatedAt: now - 2 * 60 * 60 * 1000, - latestVersion: null, - }); - expect(line).toBe(`weather v? 2h ago ${"a".repeat(49)}…`); - nowSpy.mockRestore(); - }); +describe('explore helpers', () => { + it('clamps explore limits and handles non-finite values', () => { + expect(clampLimit(-5)).toBe(1); + expect(clampLimit(0)).toBe(1); + expect(clampLimit(1)).toBe(1); + expect(clampLimit(50)).toBe(50); + expect(clampLimit(99)).toBe(99); + expect(clampLimit(200)).toBe(200); + expect(clampLimit(250)).toBe(200); + expect(clampLimit(Number.NaN)).toBe(25); + expect(clampLimit(Number.POSITIVE_INFINITY)).toBe(25); + expect(clampLimit(Number.NaN, 10)).toBe(10); + }); + + it('formats explore lines with relative time and truncation', () => { + const now = 4 * 60 * 60 * 1000; + const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(now); + const summary = 'a'.repeat(60); + const line = formatExploreLine({ + slug: 'weather', + summary, + updatedAt: now - 2 * 60 * 60 * 1000, + latestVersion: null, + }); + expect(line).toBe(`weather v? 2h ago ${'a'.repeat(49)}…`); + nowSpy.mockRestore(); + }); }); -describe("cmdExplore", () => { - it("does not attach a stored auth token to apiRequest", async () => { - mockApiRequest.mockResolvedValue({ items: [] }); +describe('cmdExplore', () => { + it('does not attach a stored auth token to apiRequest', async () => { + mockApiRequest.mockResolvedValue({ items: [] }); - await cmdExplore(makeOpts(), { limit: 25 }); + await cmdExplore(makeOpts(), { limit: 25 }); - const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - expect(requestArgs?.token).toBeUndefined(); - }); + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + expect(requestArgs?.token).toBeUndefined(); + }); - it("clamps limit and handles empty results", async () => { - mockApiRequest.mockResolvedValue({ items: [] }); + it('clamps limit and handles empty results', async () => { + mockApiRequest.mockResolvedValue({ items: [] }); - await cmdExplore(makeOpts(), { limit: 0 }); + await cmdExplore(makeOpts(), { limit: 0 }); - const [, args] = mockApiRequest.mock.calls[0] ?? []; - const url = new URL(String(args?.url)); - expect(url.searchParams.get("limit")).toBe("1"); - expect(mockLog).toHaveBeenCalledWith("No skills found."); - }); + const [, args] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(args?.url)); + expect(url.searchParams.get('limit')).toBe('1'); + expect(mockLog).toHaveBeenCalledWith('No skills found.'); + }); - it("prints formatted results", async () => { - const now = 10 * 60 * 1000; - const nowSpy = vi.spyOn(Date, "now").mockReturnValue(now); - const item = { - slug: "gog", - summary: "Google Workspace CLI for Gmail, Calendar, Drive and more.", - updatedAt: now - 90 * 1000, - latestVersion: { version: "1.2.3" }, - }; - mockApiRequest.mockResolvedValue({ items: [item] }); - - await cmdExplore(makeOpts(), { limit: 250 }); - - const [, args] = mockApiRequest.mock.calls[0] ?? []; - const url = new URL(String(args?.url)); - expect(url.searchParams.get("limit")).toBe("200"); - expect(mockLog).toHaveBeenCalledWith(formatExploreLine(item)); - nowSpy.mockRestore(); - }); - - it("supports sort and json output", async () => { - const payload = { items: [], nextCursor: null }; - mockApiRequest.mockResolvedValue(payload); - - await cmdExplore(makeOpts(), { limit: 10, sort: "installs", json: true }); - - const [, args] = mockApiRequest.mock.calls[0] ?? []; - const url = new URL(String(args?.url)); - expect(url.searchParams.get("limit")).toBe("10"); - expect(url.searchParams.get("sort")).toBe("installsCurrent"); - expect(mockLog).toHaveBeenCalledWith(JSON.stringify(payload, null, 2)); - }); - - it("supports all-time installs and trending sorts", async () => { - mockApiRequest.mockResolvedValue({ items: [], nextCursor: null }); - - await cmdExplore(makeOpts(), { limit: 5, sort: "newest" }); - await cmdExplore(makeOpts(), { limit: 5, sort: "installsAllTime" }); - await cmdExplore(makeOpts(), { limit: 5, sort: "trending" }); - - const first = new URL(String(mockApiRequest.mock.calls[0]?.[1]?.url)); - const second = new URL(String(mockApiRequest.mock.calls[1]?.[1]?.url)); - const third = new URL(String(mockApiRequest.mock.calls[2]?.[1]?.url)); - expect(first.searchParams.get("sort")).toBe("createdAt"); - expect(second.searchParams.get("sort")).toBe("installsAllTime"); - expect(third.searchParams.get("sort")).toBe("trending"); - }); -}); + it('prints formatted results', async () => { + const now = 10 * 60 * 1000; + const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(now); + const item = { + slug: 'gog', + summary: 'Google Workspace CLI for Gmail, Calendar, Drive and more.', + updatedAt: now - 90 * 1000, + latestVersion: { version: '1.2.3' }, + }; + mockApiRequest.mockResolvedValue({ items: [item] }); -describe("cmdSearch", () => { - it("does not attach a stored auth token to apiRequest", async () => { - mockApiRequest.mockResolvedValue({ results: [] }); - - await cmdSearch(makeOpts(), "demo"); - - const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - expect(requestArgs?.token).toBeUndefined(); - }); - - it("defaults limit to 25 when not specified", async () => { - mockApiRequest.mockResolvedValue({ results: [] }); - - await cmdSearch(makeOpts(), "stock price"); - - const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - const url = new URL(String(requestArgs?.url)); - expect(url.searchParams.get("limit")).toBe("25"); - }); - - it("uses explicit limit when provided", async () => { - mockApiRequest.mockResolvedValue({ results: [] }); - - await cmdSearch(makeOpts(), "stock price", 5); - - const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - const url = new URL(String(requestArgs?.url)); - expect(url.searchParams.get("limit")).toBe("5"); - }); - - it("prints skill owners in search results", async () => { - mockApiRequest.mockResolvedValue({ - results: [ - { - slug: "demo", - displayName: "Demo Skill", - version: "1.2.3", - ownerHandle: "openclaw", - score: 0.9876, - }, - { - slug: "legacy", - displayName: "Legacy Skill", - version: null, - owner: { displayName: "Legacy Owner" }, - score: 0.5, - }, - ], - }); - - await cmdSearch(makeOpts(), "demo"); - - expect(mockLog).toHaveBeenCalledWith("demo v1.2.3 @openclaw Demo Skill (0.988)"); - expect(mockLog).toHaveBeenCalledWith("legacy Legacy Owner Legacy Skill (0.500)"); - }); -}); + await cmdExplore(makeOpts(), { limit: 250 }); -describe("cmdUpdate", () => { - it("fails when directly updating a pinned skill", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { - demo: { version: "0.1.0", installedAt: 123, pinned: true, pinReason: "hold" }, - }, - }); - - await expect(cmdUpdate(makeOpts(), "demo", { force: true }, false)).rejects.toThrow( - /is pinned/i, - ); - - expect(mockApiRequest).not.toHaveBeenCalled(); - expect(mockDownloadZip).not.toHaveBeenCalled(); - }); - - it("更新下载失败时保留现有技能", async () => { - mockApiRequest.mockResolvedValue({ latestVersion: { version: "2.0.0" }, moderation: null }); - mockDownloadZip.mockRejectedValue(new Error("download failed")); - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, - }); - vi.mocked(readSkillOrigin).mockResolvedValue(null); - vi.mocked(listTextFiles).mockResolvedValue([]); - vi.mocked(stat).mockResolvedValue({} as Awaited>); - - await expect(cmdUpdate(makeOpts(), "demo", { force: true }, false)).rejects.toThrow( - "download failed", - ); - - expect(rm).not.toHaveBeenCalledWith("/work/skills/demo", { - recursive: true, - force: true, - }); - expect(renameMock).not.toHaveBeenCalled(); - }); - - it("更新解压失败时保留现有技能", async () => { - mockApiRequest.mockResolvedValue({ latestVersion: { version: "2.0.0" }, moderation: null }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, - }); - vi.mocked(readSkillOrigin).mockResolvedValue(null); - vi.mocked(listTextFiles).mockResolvedValue([]); - vi.mocked(stat).mockResolvedValue({} as Awaited>); - vi.mocked(extractZipToDir).mockRejectedValue(new Error("extract failed")); - - await expect(cmdUpdate(makeOpts(), "demo", { force: true }, false)).rejects.toThrow( - "extract failed", - ); - - expect(renameMock).not.toHaveBeenCalled(); - expect(rm).not.toHaveBeenCalledWith("/work/skills/demo", { - recursive: true, - force: true, - }); - }); - - it("skips pinned skills during update --all and reports them in the summary", async () => { - mockApiRequest.mockResolvedValue({ - latestVersion: { version: "2.0.0" }, - moderation: null, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { - demo: { version: "0.1.0", installedAt: 123, pinned: true, pinReason: "hold" }, - other: { version: "1.0.0", installedAt: 456 }, - }, - }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(readSkillOrigin).mockResolvedValue(null); - vi.mocked(writeSkillOrigin).mockResolvedValue(); - vi.mocked(extractZipToDir).mockResolvedValue(); - vi.mocked(listTextFiles).mockResolvedValue([]); - vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: "hash", files: [] }); - vi.mocked(stat).mockRejectedValue(new Error("missing")); - vi.mocked(rm).mockResolvedValue(); - - await cmdUpdate(makeOpts(), undefined, { all: true }, false); - - expect(mockApiRequest).toHaveBeenCalledTimes(1); - const [, args] = mockApiRequest.mock.calls[0] ?? []; - expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent("other")}`); - expect(writeLockfile).toHaveBeenCalledWith("/work", { - version: 1, - skills: { - demo: { version: "0.1.0", installedAt: 123, pinned: true, pinReason: "hold" }, - other: { version: "2.0.0", installedAt: expect.any(Number) }, - }, - }); - expect(mockLog).toHaveBeenCalledWith("Skipped 1 pinned skill: demo"); - }); - - it("uses path-based skill lookup when no local fingerprint is available", async () => { - mockApiRequest.mockResolvedValue({ latestVersion: { version: "1.0.0" } }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "0.1.0", installedAt: 123 } }, - }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(readSkillOrigin).mockResolvedValue(null); - vi.mocked(writeSkillOrigin).mockResolvedValue(); - vi.mocked(extractZipToDir).mockResolvedValue(); - vi.mocked(listTextFiles).mockResolvedValue([]); - vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: "hash", files: [] }); - vi.mocked(stat).mockRejectedValue(new Error("missing")); - vi.mocked(rm).mockResolvedValue(); - - await cmdUpdate(makeOpts(), "demo", {}, false); - - const [, args] = mockApiRequest.mock.calls[0] ?? []; - expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent("demo")}`); - expect(args?.url).toBeUndefined(); - }); - - it("trusts the stored install fingerprint when the resolve endpoint cannot match", async () => { - mockApiRequest - .mockResolvedValueOnce({ - latestVersion: { version: "2.0.0" }, - moderation: null, - }) - .mockResolvedValueOnce({ - match: null, - latestVersion: { version: "2.0.0" }, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, - }); - vi.mocked(readSkillOrigin).mockResolvedValue({ - version: 1, - registry: "https://example.com", - slug: "demo", - installedVersion: "1.0.0", - installedAt: 123, - fingerprint: "hash", - }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(writeSkillOrigin).mockResolvedValue(); - vi.mocked(extractZipToDir).mockResolvedValue(); - vi.mocked(listTextFiles).mockResolvedValue([ - { relPath: "SKILL.md", bytes: new Uint8Array([1]) }, - ]); - vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: "hash", files: [] }); - vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); - vi.mocked(rm).mockResolvedValue(); - - await cmdUpdate(makeOpts(), "demo", {}, false); - - expect(mockLog).not.toHaveBeenCalledWith( - "demo: local changes (no match). Use --force to overwrite.", - ); - expect(mockDownloadZip).toHaveBeenCalledWith( - "https://example.com", - expect.objectContaining({ slug: "demo", version: "2.0.0" }), - ); - expect(writeSkillOrigin).toHaveBeenCalledWith("/work/skills/.demo-update-test", { - version: 1, - registry: "https://example.com", - slug: "demo", - installedVersion: "2.0.0", - installedAt: 123, - fingerprint: "hash", - }); - expect(renameMock).toHaveBeenNthCalledWith( - 1, - "/work/skills/demo", - "/work/skills/.demo-update-test-previous", - ); - expect(renameMock).toHaveBeenNthCalledWith( - 2, - "/work/skills/.demo-update-test", - "/work/skills/demo", - ); - }); -}); + const [, args] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(args?.url)); + expect(url.searchParams.get('limit')).toBe('200'); + expect(mockLog).toHaveBeenCalledWith(formatExploreLine(item)); + nowSpy.mockRestore(); + }); -describe("pin commands", () => { - it("pins an installed skill and preserves its version metadata", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, - }); - vi.mocked(writeLockfile).mockResolvedValue(); - - await cmdPin(makeOpts(), "demo", { reason: "scanner hold" }); - - expect(writeLockfile).toHaveBeenCalledWith("/work", { - version: 1, - skills: { - demo: { - version: "1.0.0", - installedAt: 123, - pinned: true, - pinReason: "scanner hold", - }, - }, - }); - expect(mockLog).toHaveBeenCalledWith("Pinned demo: scanner hold"); - }); - - it("reports when an installed skill is already pinned without changes", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { - demo: { version: "1.0.0", installedAt: 123, pinned: true, pinReason: "scanner hold" }, - }, - }); - - await cmdPin(makeOpts(), "demo"); - - expect(writeLockfile).not.toHaveBeenCalled(); - expect(mockLog).toHaveBeenCalledWith('Skill "demo" is already pinned: scanner hold'); - }); - - it("unpinned skills clear pin metadata and keep the installed version", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { - demo: { version: "1.0.0", installedAt: 123, pinned: true, pinReason: "scanner hold" }, - }, - }); - vi.mocked(writeLockfile).mockResolvedValue(); - - await cmdUnpin(makeOpts(), "demo"); - - expect(writeLockfile).toHaveBeenCalledWith("/work", { - version: 1, - skills: { - demo: { - version: "1.0.0", - installedAt: 123, - }, - }, - }); - expect(mockLog).toHaveBeenCalledWith("Unpinned demo"); - }); -}); + it('supports sort and json output', async () => { + const payload = { items: [], nextCursor: null }; + mockApiRequest.mockResolvedValue(payload); + + await cmdExplore(makeOpts(), { limit: 10, sort: 'installs', json: true }); -describe("cmdList", () => { - it("shows pinned state in list output", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { - demo: { version: "1.0.0", installedAt: 123, pinned: true, pinReason: "scanner hold" }, - other: { version: "2.0.0", installedAt: 456 }, - }, + const [, args] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(args?.url)); + expect(url.searchParams.get('limit')).toBe('10'); + expect(url.searchParams.get('sort')).toBe('installsCurrent'); + expect(mockLog).toHaveBeenCalledWith(JSON.stringify(payload, null, 2)); }); - await cmdList(makeOpts()); + it('supports all-time installs and trending sorts', async () => { + mockApiRequest.mockResolvedValue({ items: [], nextCursor: null }); - expect(mockLog).toHaveBeenCalledWith("demo 1.0.0 pinned (scanner hold)"); - expect(mockLog).toHaveBeenCalledWith("other 2.0.0"); - }); -}); + await cmdExplore(makeOpts(), { limit: 5, sort: 'newest' }); + await cmdExplore(makeOpts(), { limit: 5, sort: 'installsAllTime' }); + await cmdExplore(makeOpts(), { limit: 5, sort: 'trending' }); -describe("cmdInstall", () => { - it("does not attach a stored auth token to API or download requests", async () => { - mockApiRequest.mockResolvedValue({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: {}, - stats: {}, - createdAt: 0, - updatedAt: 0, - }, - latestVersion: { version: "1.0.0" }, - owner: null, - moderation: null, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(writeSkillOrigin).mockResolvedValue(); - vi.mocked(extractZipToDir).mockResolvedValue(); - vi.mocked(stat).mockRejectedValue(new Error("missing")); - vi.mocked(rm).mockResolvedValue(); - - await cmdInstall(makeOpts(), "demo"); - - const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; - expect(requestArgs?.token).toBeUndefined(); - const [, zipArgs] = mockDownloadZip.mock.calls[0] ?? []; - expect(zipArgs?.token).toBeUndefined(); - }); - - it("blocks force reinstall when a skill is pinned", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "0.9.0", installedAt: 123, pinned: true, pinReason: "hold" } }, - }); - vi.mocked(stat).mockRejectedValue(new Error("missing")); - - await expect(cmdInstall(makeOpts(), "demo", undefined, true)).rejects.toThrow(/is pinned/i); - - expect(mockApiRequest).not.toHaveBeenCalled(); - expect(mockDownloadZip).not.toHaveBeenCalled(); - expect(rm).not.toHaveBeenCalled(); - expect(writeLockfile).not.toHaveBeenCalled(); - }); - - it("does not rm local directory when skill is malware-blocked (--force)", async () => { - vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists - mockApiRequest.mockResolvedValue({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: {}, - stats: {}, - createdAt: 0, - updatedAt: 0, - }, - latestVersion: { version: "1.0.0" }, - owner: null, - moderation: { isMalwareBlocked: true, isSuspicious: false }, - }); - - await expect(cmdInstall(makeOpts(), "demo", undefined, true)).rejects.toThrow(/malware/i); - - expect(rm).not.toHaveBeenCalled(); - }); - - it("does not rm local directory when API fetch fails (--force)", async () => { - vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists - mockApiRequest.mockRejectedValue(new Error("Skill not found")); - - await expect(cmdInstall(makeOpts(), "demo", undefined, true)).rejects.toThrow(/not found/i); - - expect(rm).not.toHaveBeenCalled(); - }); - - it("does not rm local directory when requested version lookup fails (--force)", async () => { - vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists - mockApiRequest - .mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: {}, - stats: {}, - createdAt: 0, - updatedAt: 0, - }, - latestVersion: { version: "1.0.0" }, - owner: null, - moderation: null, - }) - .mockRejectedValueOnce(new Error("Version not found")); - - await expect(cmdInstall(makeOpts(), "demo", "9.9.9", true)).rejects.toThrow( - /version not found/i, - ); - - expect(rm).not.toHaveBeenCalled(); - expect(mockApiRequest).toHaveBeenNthCalledWith( - 2, - "https://example.com", - expect.objectContaining({ - path: `${ApiRoutes.skills}/${encodeURIComponent("demo")}/versions/${encodeURIComponent("9.9.9")}`, - }), - expect.anything(), - ); - }); - - it("validates requested version before rm when all checks pass (--force)", async () => { - vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists - mockApiRequest - .mockResolvedValueOnce({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: {}, - stats: {}, - createdAt: 0, - updatedAt: 0, - }, - latestVersion: { version: "1.0.0" }, - owner: null, - moderation: null, - }) - .mockResolvedValueOnce({ - version: { - version: "9.9.9", - createdAt: 0, - changelog: "", - changelogSource: null, - license: null, - files: [], - }, - skill: { slug: "demo", displayName: "Demo" }, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(writeSkillOrigin).mockResolvedValue(); - vi.mocked(extractZipToDir).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - - await cmdInstall(makeOpts(), "demo", "9.9.9", true); - - expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); - expect(mockDownloadZip).toHaveBeenCalledWith( - "https://example.com", - expect.objectContaining({ slug: "demo", version: "9.9.9" }), - ); - const versionLookupOrder = mockApiRequest.mock.invocationCallOrder[1]; - const rmOrder = vi.mocked(rm).mock.invocationCallOrder[0]; - const downloadOrder = mockDownloadZip.mock.invocationCallOrder[0]; - expect(versionLookupOrder).toBeLessThan(rmOrder); - expect(rmOrder).toBeLessThan(downloadOrder); - }); - - it("calls rm before download when all checks pass (--force)", async () => { - vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists - mockApiRequest.mockResolvedValue({ - skill: { - slug: "demo", - displayName: "Demo", - summary: null, - tags: {}, - stats: {}, - createdAt: 0, - updatedAt: 0, - }, - latestVersion: { version: "1.0.0" }, - owner: null, - moderation: null, - }); - mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); - vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(writeSkillOrigin).mockResolvedValue(); - vi.mocked(extractZipToDir).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - - await cmdInstall(makeOpts(), "demo", undefined, true); - - expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); - expect(mockDownloadZip).toHaveBeenCalled(); - const rmOrder = vi.mocked(rm).mock.invocationCallOrder[0]; - const downloadOrder = mockDownloadZip.mock.invocationCallOrder[0]; - expect(rmOrder).toBeLessThan(downloadOrder); - }); + const first = new URL(String(mockApiRequest.mock.calls[0]?.[1]?.url)); + const second = new URL(String(mockApiRequest.mock.calls[1]?.[1]?.url)); + const third = new URL(String(mockApiRequest.mock.calls[2]?.[1]?.url)); + expect(first.searchParams.get('sort')).toBe('createdAt'); + expect(second.searchParams.get('sort')).toBe('installsAllTime'); + expect(third.searchParams.get('sort')).toBe('trending'); + }); }); -describe("cmdUninstall", () => { - it("requires --yes when input is disabled", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, +describe('cmdSearch', () => { + it('does not attach a stored auth token to apiRequest', async () => { + mockApiRequest.mockResolvedValue({ results: [] }); + + await cmdSearch(makeOpts(), 'demo'); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + expect(requestArgs?.token).toBeUndefined(); }); - await expect(cmdUninstall(makeOpts(), "demo", {}, false)).rejects.toThrow(/--yes/i); - }); + it('defaults limit to 25 when not specified', async () => { + mockApiRequest.mockResolvedValue({ results: [] }); + + await cmdSearch(makeOpts(), 'stock price'); - it("prompts when interactive and proceeds on confirm", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(requestArgs?.url)); + expect(url.searchParams.get('limit')).toBe('25'); }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - mockIsInteractive.mockReturnValue(true); - mockPromptConfirm.mockResolvedValue(true); - await cmdUninstall(makeOpts(), "demo", {}, true); + it('uses explicit limit when provided', async () => { + mockApiRequest.mockResolvedValue({ results: [] }); - expect(mockPromptConfirm).toHaveBeenCalledWith("Uninstall demo?"); - expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); - expect(writeLockfile).toHaveBeenCalled(); - }); + await cmdSearch(makeOpts(), 'stock price', 5); - it("prints Cancelled and does not remove when prompt declines", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + const url = new URL(String(requestArgs?.url)); + expect(url.searchParams.get('limit')).toBe('5'); }); - mockIsInteractive.mockReturnValue(true); - mockPromptConfirm.mockResolvedValue(false); - await cmdUninstall(makeOpts(), "demo", {}, true); + it('prints skill owners in search results', async () => { + mockApiRequest.mockResolvedValue({ + results: [ + { + slug: 'demo', + displayName: 'Demo Skill', + version: '1.2.3', + ownerHandle: 'openclaw', + score: 0.9876, + }, + { + slug: 'legacy', + displayName: 'Legacy Skill', + version: null, + owner: { displayName: 'Legacy Owner' }, + score: 0.5, + }, + ], + }); - expect(mockLog).toHaveBeenCalledWith("Cancelled."); - expect(rm).not.toHaveBeenCalled(); - expect(writeLockfile).not.toHaveBeenCalled(); - }); + await cmdSearch(makeOpts(), 'demo'); - it("rejects unsafe slugs", async () => { - await expect(cmdUninstall(makeOpts(), "../evil", { yes: true }, false)).rejects.toThrow( - /invalid slug/i, - ); - await expect(cmdUninstall(makeOpts(), "demo/evil", { yes: true }, false)).rejects.toThrow( - /invalid slug/i, - ); - }); + expect(mockLog).toHaveBeenCalledWith('demo v1.2.3 @openclaw Demo Skill (0.988)'); + expect(mockLog).toHaveBeenCalledWith('legacy Legacy Owner Legacy Skill (0.500)'); + }); +}); - it("fails when skill is not installed", async () => { - vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); +describe('cmdUpdate', () => { + it('fails when directly updating a pinned skill', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: '0.1.0', installedAt: 123, pinned: true, pinReason: 'hold' }, + }, + }); + + await expect(cmdUpdate(makeOpts(), 'demo', { force: true }, false)).rejects.toThrow( + /is pinned/i + ); + + expect(mockApiRequest).not.toHaveBeenCalled(); + expect(mockDownloadZip).not.toHaveBeenCalled(); + }); + + it('更新下载失败时保留现有技能', async () => { + mockApiRequest.mockResolvedValue({ latestVersion: { version: '2.0.0' }, moderation: null }); + mockDownloadZip.mockRejectedValue(new Error('download failed')); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(stat).mockResolvedValue({} as Awaited>); + + await expect(cmdUpdate(makeOpts(), 'demo', { force: true }, false)).rejects.toThrow( + 'download failed' + ); + + expect(rm).not.toHaveBeenCalledWith('/work/skills/demo', { + recursive: true, + force: true, + }); + expect(renameMock).not.toHaveBeenCalled(); + }); + + it('更新解压失败时保留现有技能', async () => { + mockApiRequest.mockResolvedValue({ latestVersion: { version: '2.0.0' }, moderation: null }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(stat).mockResolvedValue({} as Awaited>); + vi.mocked(extractZipToDir).mockRejectedValue(new Error('extract failed')); + + await expect(cmdUpdate(makeOpts(), 'demo', { force: true }, false)).rejects.toThrow( + 'extract failed' + ); + + expect(renameMock).not.toHaveBeenCalled(); + expect(rm).not.toHaveBeenCalledWith('/work/skills/demo', { + recursive: true, + force: true, + }); + }); + + it('skips pinned skills during update --all and reports them in the summary', async () => { + mockApiRequest.mockResolvedValue({ + latestVersion: { version: '2.0.0' }, + moderation: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: '0.1.0', installedAt: 123, pinned: true, pinReason: 'hold' }, + other: { version: '1.0.0', installedAt: 456 }, + }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: 'hash', files: [] }); + vi.mocked(stat).mockRejectedValue(new Error('missing')); + vi.mocked(rm).mockResolvedValue(); + + await cmdUpdate(makeOpts(), undefined, { all: true }, false); + + expect(mockApiRequest).toHaveBeenCalledTimes(1); + const [, args] = mockApiRequest.mock.calls[0] ?? []; + expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent('other')}`); + expect(writeLockfile).toHaveBeenCalledWith('/work', { + version: 1, + skills: { + demo: { version: '0.1.0', installedAt: 123, pinned: true, pinReason: 'hold' }, + other: { version: '2.0.0', installedAt: expect.any(Number) }, + }, + }); + expect(mockLog).toHaveBeenCalledWith('Skipped 1 pinned skill: demo'); + }); + + it('uses path-based skill lookup when no local fingerprint is available', async () => { + mockApiRequest.mockResolvedValue({ latestVersion: { version: '1.0.0' } }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '0.1.0', installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(readSkillOrigin).mockResolvedValue(null); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(listTextFiles).mockResolvedValue([]); + vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: 'hash', files: [] }); + vi.mocked(stat).mockRejectedValue(new Error('missing')); + vi.mocked(rm).mockResolvedValue(); + + await cmdUpdate(makeOpts(), 'demo', {}, false); + + const [, args] = mockApiRequest.mock.calls[0] ?? []; + expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent('demo')}`); + expect(args?.url).toBeUndefined(); + }); + + it('trusts the stored install fingerprint when the resolve endpoint cannot match', async () => { + mockApiRequest + .mockResolvedValueOnce({ + latestVersion: { version: '2.0.0' }, + moderation: null, + }) + .mockResolvedValueOnce({ + match: null, + latestVersion: { version: '2.0.0' }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(readSkillOrigin).mockResolvedValue({ + version: 1, + registry: 'https://example.com', + slug: 'demo', + installedVersion: '1.0.0', + installedAt: 123, + fingerprint: 'hash', + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(listTextFiles).mockResolvedValue([ + { relPath: 'SKILL.md', bytes: new Uint8Array([1]) }, + ]); + vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: 'hash', files: [] }); + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); + vi.mocked(rm).mockResolvedValue(); + + await cmdUpdate(makeOpts(), 'demo', {}, false); + + expect(mockLog).not.toHaveBeenCalledWith( + 'demo: local changes (no match). Use --force to overwrite.' + ); + expect(mockDownloadZip).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ slug: 'demo', version: '2.0.0' }) + ); + expect(writeSkillOrigin).toHaveBeenCalledWith('/work/skills/.demo-update-test', { + version: 1, + registry: 'https://example.com', + slug: 'demo', + installedVersion: '2.0.0', + installedAt: 123, + fingerprint: 'hash', + }); + expect(renameMock).toHaveBeenNthCalledWith( + 1, + '/work/skills/demo', + '/work/skills/.demo-update-test-previous' + ); + expect(renameMock).toHaveBeenNthCalledWith( + 2, + '/work/skills/.demo-update-test', + '/work/skills/demo' + ); + }); +}); - await expect(cmdUninstall(makeOpts(), "missing", {}, false)).rejects.toThrow( - "Not installed: missing", - ); - }); +describe('pin commands', () => { + it('pins an installed skill and preserves its version metadata', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + + await cmdPin(makeOpts(), 'demo', { reason: 'scanner hold' }); + + expect(writeLockfile).toHaveBeenCalledWith('/work', { + version: 1, + skills: { + demo: { + version: '1.0.0', + installedAt: 123, + pinned: true, + pinReason: 'scanner hold', + }, + }, + }); + expect(mockLog).toHaveBeenCalledWith('Pinned demo: scanner hold'); + }); + + it('reports when an installed skill is already pinned without changes', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { + version: '1.0.0', + installedAt: 123, + pinned: true, + pinReason: 'scanner hold', + }, + }, + }); + + await cmdPin(makeOpts(), 'demo'); + + expect(writeLockfile).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledWith('Skill "demo" is already pinned: scanner hold'); + }); + + it('unpinned skills clear pin metadata and keep the installed version', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { + version: '1.0.0', + installedAt: 123, + pinned: true, + pinReason: 'scanner hold', + }, + }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + + await cmdUnpin(makeOpts(), 'demo'); + + expect(writeLockfile).toHaveBeenCalledWith('/work', { + version: 1, + skills: { + demo: { + version: '1.0.0', + installedAt: 123, + }, + }, + }); + expect(mockLog).toHaveBeenCalledWith('Unpinned demo'); + }); +}); + +describe('cmdList', () => { + it('shows pinned state in list output', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { + version: '1.0.0', + installedAt: 123, + pinned: true, + pinReason: 'scanner hold', + }, + other: { version: '2.0.0', installedAt: 456 }, + }, + }); + + await cmdList(makeOpts()); + + expect(mockLog).toHaveBeenCalledWith('demo 1.0.0 pinned (scanner hold)'); + expect(mockLog).toHaveBeenCalledWith('other 2.0.0'); + }); +}); + +describe('cmdInstall', () => { + it('does not attach a stored auth token to API or download requests', async () => { + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: '1.0.0' }, + owner: null, + moderation: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(stat).mockRejectedValue(new Error('missing')); + vi.mocked(rm).mockResolvedValue(); + + await cmdInstall(makeOpts(), 'demo'); + + const [, requestArgs] = mockApiRequest.mock.calls[0] ?? []; + expect(requestArgs?.token).toBeUndefined(); + const [, zipArgs] = mockDownloadZip.mock.calls[0] ?? []; + expect(zipArgs?.token).toBeUndefined(); + }); + + it('blocks force reinstall when a skill is pinned', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: '0.9.0', installedAt: 123, pinned: true, pinReason: 'hold' }, + }, + }); + vi.mocked(stat).mockRejectedValue(new Error('missing')); + + await expect(cmdInstall(makeOpts(), 'demo', undefined, true)).rejects.toThrow(/is pinned/i); + + expect(mockApiRequest).not.toHaveBeenCalled(); + expect(mockDownloadZip).not.toHaveBeenCalled(); + expect(rm).not.toHaveBeenCalled(); + expect(writeLockfile).not.toHaveBeenCalled(); + }); + + it('does not rm local directory when skill is malware-blocked (--force)', async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: '1.0.0' }, + owner: null, + moderation: { isMalwareBlocked: true, isSuspicious: false }, + }); + + await expect(cmdInstall(makeOpts(), 'demo', undefined, true)).rejects.toThrow(/malware/i); + + expect(rm).not.toHaveBeenCalled(); + }); + + it('does not rm local directory when API fetch fails (--force)', async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest.mockRejectedValue(new Error('Skill not found')); + + await expect(cmdInstall(makeOpts(), 'demo', undefined, true)).rejects.toThrow(/not found/i); + + expect(rm).not.toHaveBeenCalled(); + }); + + it('does not rm local directory when requested version lookup fails (--force)', async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest + .mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: '1.0.0' }, + owner: null, + moderation: null, + }) + .mockRejectedValueOnce(new Error('Version not found')); + + await expect(cmdInstall(makeOpts(), 'demo', '9.9.9', true)).rejects.toThrow( + /version not found/i + ); + + expect(rm).not.toHaveBeenCalled(); + expect(mockApiRequest).toHaveBeenNthCalledWith( + 2, + 'https://example.com', + expect.objectContaining({ + path: `${ApiRoutes.skills}/${encodeURIComponent( + 'demo' + )}/versions/${encodeURIComponent('9.9.9')}`, + }), + expect.anything() + ); + }); + + it('validates requested version before rm when all checks pass (--force)', async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest + .mockResolvedValueOnce({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: '1.0.0' }, + owner: null, + moderation: null, + }) + .mockResolvedValueOnce({ + version: { + version: '9.9.9', + createdAt: 0, + changelog: '', + changelogSource: null, + license: null, + files: [], + }, + skill: { slug: 'demo', displayName: 'Demo' }, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdInstall(makeOpts(), 'demo', '9.9.9', true); + + expect(rm).toHaveBeenCalledWith('/work/skills/demo', { recursive: true, force: true }); + expect(mockDownloadZip).toHaveBeenCalledWith( + 'https://example.com', + expect.objectContaining({ slug: 'demo', version: '9.9.9' }) + ); + const versionLookupOrder = mockApiRequest.mock.invocationCallOrder[1]; + const rmOrder = vi.mocked(rm).mock.invocationCallOrder[0]; + const downloadOrder = mockDownloadZip.mock.invocationCallOrder[0]; + expect(versionLookupOrder).toBeLessThan(rmOrder); + expect(rmOrder).toBeLessThan(downloadOrder); + }); + + it('calls rm before download when all checks pass (--force)', async () => { + vi.mocked(stat).mockResolvedValue({} as unknown as Awaited>); // target exists + mockApiRequest.mockResolvedValue({ + skill: { + slug: 'demo', + displayName: 'Demo', + summary: null, + tags: {}, + stats: {}, + createdAt: 0, + updatedAt: 0, + }, + latestVersion: { version: '1.0.0' }, + owner: null, + moderation: null, + }); + mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3])); + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(writeSkillOrigin).mockResolvedValue(); + vi.mocked(extractZipToDir).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdInstall(makeOpts(), 'demo', undefined, true); + + expect(rm).toHaveBeenCalledWith('/work/skills/demo', { recursive: true, force: true }); + expect(mockDownloadZip).toHaveBeenCalled(); + const rmOrder = vi.mocked(rm).mock.invocationCallOrder[0]; + const downloadOrder = mockDownloadZip.mock.invocationCallOrder[0]; + expect(rmOrder).toBeLessThan(downloadOrder); + }); +}); + +describe('cmdUninstall', () => { + it('requires --yes when input is disabled', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + + await expect(cmdUninstall(makeOpts(), 'demo', {}, false)).rejects.toThrow(/--yes/i); + }); - it("removes skill directory and lockfile entry with --yes flag", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, + it('prompts when interactive and proceeds on confirm', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + mockIsInteractive.mockReturnValue(true); + mockPromptConfirm.mockResolvedValue(true); + + await cmdUninstall(makeOpts(), 'demo', {}, true); + + expect(mockPromptConfirm).toHaveBeenCalledWith('Uninstall demo?'); + expect(rm).toHaveBeenCalledWith('/work/skills/demo', { recursive: true, force: true }); + expect(writeLockfile).toHaveBeenCalled(); }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - await cmdUninstall(makeOpts(), "demo", { yes: true }, false); + it('prints Cancelled and does not remove when prompt declines', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + mockIsInteractive.mockReturnValue(true); + mockPromptConfirm.mockResolvedValue(false); + + await cmdUninstall(makeOpts(), 'demo', {}, true); - expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); - expect(writeLockfile).toHaveBeenCalledWith("/work", { - version: 1, - skills: {}, + expect(mockLog).toHaveBeenCalledWith('Cancelled.'); + expect(rm).not.toHaveBeenCalled(); + expect(writeLockfile).not.toHaveBeenCalled(); }); - expect(mockSpinner.succeed).toHaveBeenCalledWith("Uninstalled demo"); - }); - it("does not update lockfile if remove fails", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, + it('rejects unsafe slugs', async () => { + await expect(cmdUninstall(makeOpts(), '../evil', { yes: true }, false)).rejects.toThrow( + /invalid slug/i + ); + await expect(cmdUninstall(makeOpts(), 'demo/evil', { yes: true }, false)).rejects.toThrow( + /invalid slug/i + ); + }); + + it('fails when skill is not installed', async () => { + vi.mocked(readLockfile).mockResolvedValue({ version: 1, skills: {} }); + + await expect(cmdUninstall(makeOpts(), 'missing', {}, false)).rejects.toThrow( + 'Not installed: missing' + ); }); - vi.mocked(rm).mockRejectedValue(new Error("nope")); - await expect(cmdUninstall(makeOpts(), "demo", { yes: true }, false)).rejects.toThrow("nope"); + it('removes skill directory and lockfile entry with --yes flag', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); - expect(writeLockfile).not.toHaveBeenCalled(); - }); + await cmdUninstall(makeOpts(), 'demo', { yes: true }, false); - it("updates lockfile after removing directory", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, + expect(rm).toHaveBeenCalledWith('/work/skills/demo', { recursive: true, force: true }); + expect(writeLockfile).toHaveBeenCalledWith('/work', { + version: 1, + skills: {}, + }); + expect(mockSpinner.succeed).toHaveBeenCalledWith('Uninstalled demo'); }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - await cmdUninstall(makeOpts(), "demo", { yes: true }, false); + it('does not update lockfile if remove fails', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(rm).mockRejectedValue(new Error('nope')); - const rmCallMock = vi.mocked(rm); - const writeLockfileCallMock = vi.mocked(writeLockfile); - expect(rmCallMock.mock.invocationCallOrder[0]).toBeLessThan( - writeLockfileCallMock.mock.invocationCallOrder[0], - ); - }); + await expect(cmdUninstall(makeOpts(), 'demo', { yes: true }, false)).rejects.toThrow( + 'nope' + ); - it("removes skill and updates lockfile keeping other skills", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { - demo: { version: "1.0.0", installedAt: 123 }, - other: { version: "2.0.0", installedAt: 456 }, - }, + expect(writeLockfile).not.toHaveBeenCalled(); }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - await cmdUninstall(makeOpts(), "demo", { yes: true }, false); + it('updates lockfile after removing directory', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); - expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); - expect(writeLockfile).toHaveBeenCalledWith("/work", { - version: 1, - skills: { other: { version: "2.0.0", installedAt: 456 } }, + await cmdUninstall(makeOpts(), 'demo', { yes: true }, false); + + const rmCallMock = vi.mocked(rm); + const writeLockfileCallMock = vi.mocked(writeLockfile); + expect(rmCallMock.mock.invocationCallOrder[0]).toBeLessThan( + writeLockfileCallMock.mock.invocationCallOrder[0] + ); }); - }); - it("trims slug whitespace", async () => { - vi.mocked(readLockfile).mockResolvedValue({ - version: 1, - skills: { demo: { version: "1.0.0", installedAt: 123 } }, + it('removes skill and updates lockfile keeping other skills', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { + demo: { version: '1.0.0', installedAt: 123 }, + other: { version: '2.0.0', installedAt: 456 }, + }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); + + await cmdUninstall(makeOpts(), 'demo', { yes: true }, false); + + expect(rm).toHaveBeenCalledWith('/work/skills/demo', { recursive: true, force: true }); + expect(writeLockfile).toHaveBeenCalledWith('/work', { + version: 1, + skills: { other: { version: '2.0.0', installedAt: 456 } }, + }); }); - vi.mocked(writeLockfile).mockResolvedValue(); - vi.mocked(rm).mockResolvedValue(); - await cmdUninstall(makeOpts(), " demo ", { yes: true }, false); + it('trims slug whitespace', async () => { + vi.mocked(readLockfile).mockResolvedValue({ + version: 1, + skills: { demo: { version: '1.0.0', installedAt: 123 } }, + }); + vi.mocked(writeLockfile).mockResolvedValue(); + vi.mocked(rm).mockResolvedValue(); - expect(rm).toHaveBeenCalledWith("/work/skills/demo", { recursive: true, force: true }); - }); + await cmdUninstall(makeOpts(), ' demo ', { yes: true }, false); + + expect(rm).toHaveBeenCalledWith('/work/skills/demo', { recursive: true, force: true }); + }); }); diff --git a/dt-skill/src/cli/commands/skills.ts b/dt-skill/src/cli/commands/skills.ts index de0a5bb4..57b45d08 100644 --- a/dt-skill/src/cli/commands/skills.ts +++ b/dt-skill/src/cli/commands/skills.ts @@ -1,917 +1,964 @@ -import { mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises"; -import { basename, dirname, join } from "node:path"; -import semver from "semver"; -import { apiRequest, downloadZip, registryUrl } from "../../http.js"; +import { mkdir, mkdtemp, rename, rm, stat } from 'node:fs/promises'; +import { basename, dirname, join } from 'node:path'; +import semver from 'semver'; +import { apiRequest, downloadZip, registryUrl } from '../../http.js'; import { - ApiRoutes, - ApiV1SearchResponseSchema, - ApiV1SkillListResponseSchema, - ApiV1SkillResolveResponseSchema, - ApiV1SkillResponseSchema, - ApiV1SkillVersionResponseSchema, - type ApiV1SearchResponse, - type ApiV1SkillListResponse, - type ApiV1SkillResponse, - type ApiV1SkillResolveResponse, -} from "../../schema/index.js"; + ApiRoutes, + ApiV1SearchResponseSchema, + ApiV1SkillListResponseSchema, + ApiV1SkillResolveResponseSchema, + ApiV1SkillResponseSchema, + ApiV1SkillVersionResponseSchema, + type ApiV1SearchResponse, + type ApiV1SkillListResponse, + type ApiV1SkillResponse, + type ApiV1SkillResolveResponse, +} from '../../schema/index.js'; import { - extractZipToDir, - hashSkillFiles, - listManualSkills, - listTextFiles, - readLockfile, - readSkillOrigin, - writeLockfile, - writeSkillOrigin, -} from "../../skills.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts, ResolveResult } from "../types.js"; + extractZipToDir, + hashSkillFiles, + listManualSkills, + listTextFiles, + readLockfile, + readSkillOrigin, + writeLockfile, + writeSkillOrigin, +} from '../../skills.js'; +import { getRegistry } from '../registry.js'; +import type { GlobalOpts, ResolveResult } from '../types.js'; import { - createSpinner, - fail, - formatError, - isInteractive, - promptConfirm, - selectAgent, - selectScope, -} from "../ui.js"; -import { searchMultiselect, cancelSymbol } from "../prompts/search-multiselect.js"; -import { getAgentLabel, resolveAgentWorkdir } from "../agents.js"; -import type { AgentName } from "../agents.js"; + createSpinner, + fail, + formatError, + isInteractive, + promptConfirm, + selectAgent, + selectScope, +} from '../ui.js'; +import { searchMultiselect, cancelSymbol } from '../prompts/search-multiselect.js'; +import { getAgentLabel, resolveAgentWorkdir } from '../agents.js'; +import type { AgentName } from '../agents.js'; function normalizeSkillSlugOrFail(raw: string) { - const slug = raw.trim(); - if (!slug) fail("Slug required"); - // Safety: never allow path traversal or nested paths to become filesystem operations. - if (slug.includes("/") || slug.includes("\\") || slug.includes("..")) { - fail(`Invalid slug: ${slug}`); - } - return slug; + const slug = raw.trim(); + if (!slug) fail('Slug required'); + // Safety: never allow path traversal or nested paths to become filesystem operations. + if (slug.includes('/') || slug.includes('\\') || slug.includes('..')) { + fail(`Invalid slug: ${slug}`); + } + return slug; } function isSafeSkillSlug(slug: string) { - return Boolean(slug) && !slug.includes("/") && !slug.includes("\\") && !slug.includes(".."); + return Boolean(slug) && !slug.includes('/') && !slug.includes('\\') && !slug.includes('..'); } function isPinnedSkillEntry(entry?: { pinned?: boolean | null }) { - return entry?.pinned === true; + return entry?.pinned === true; } function withPinnedMetadata( - version: string | null, - installedAt: number, - existing?: { pinned?: boolean; pinReason?: string }, + version: string | null, + installedAt: number, + existing?: { pinned?: boolean; pinReason?: string } ) { - return { - version, - installedAt, - ...(existing?.pinned ? { pinned: true } : {}), - ...(existing?.pinned && existing.pinReason ? { pinReason: existing.pinReason } : {}), - }; + return { + version, + installedAt, + ...(existing?.pinned ? { pinned: true } : {}), + ...(existing?.pinned && existing.pinReason ? { pinReason: existing.pinReason } : {}), + }; } function formatPinnedDetails(entry?: { pinReason?: string }) { - return entry?.pinReason ? ` (${entry.pinReason})` : ""; + return entry?.pinReason ? ` (${entry.pinReason})` : ''; } function formatSearchOwner(entry: { - ownerHandle?: string | null; - owner?: { handle?: string | null; displayName?: string | null } | null; + ownerHandle?: string | null; + owner?: { handle?: string | null; displayName?: string | null } | null; }) { - const handle = entry.ownerHandle ?? entry.owner?.handle; - if (handle) return `@${handle}`; - return entry.owner?.displayName ?? "unknown owner"; + const handle = entry.ownerHandle ?? entry.owner?.handle; + if (handle) return `@${handle}`; + return entry.owner?.displayName ?? 'unknown owner'; } export async function cmdSearch(opts: GlobalOpts, query: string, limit?: number) { - if (!query) fail("Query required"); - - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Searching"); - try { - const url = registryUrl(ApiRoutes.search, registry); - url.searchParams.set("q", query); - const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? limit : 25; - url.searchParams.set("limit", String(effectiveLimit)); - const result = await apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1SearchResponseSchema, - ); + if (!query) fail('Query required'); + + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner('Searching'); + try { + const url = registryUrl(ApiRoutes.search, registry); + url.searchParams.set('q', query); + const effectiveLimit = typeof limit === 'number' && Number.isFinite(limit) ? limit : 25; + url.searchParams.set('limit', String(effectiveLimit)); + const result = await apiRequest( + registry, + { method: 'GET', url: url.toString() }, + ApiV1SearchResponseSchema + ); - spinner.stop(); - for (const entry of result.results) { - const slug = entry.slug ?? "unknown"; - const name = entry.displayName ?? slug; - const version = entry.version ? ` v${entry.version}` : ""; - console.log( - `${slug}${version} ${formatSearchOwner(entry)} ${name} (${entry.score.toFixed(3)})`, - ); - } - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + spinner.stop(); + for (const entry of result.results) { + const slug = entry.slug ?? 'unknown'; + const name = entry.displayName ?? slug; + const version = entry.version ? ` v${entry.version}` : ''; + console.log( + `${slug}${version} ${formatSearchOwner(entry)} ${name} (${entry.score.toFixed( + 3 + )})` + ); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } export async function cmdInstall( - opts: GlobalOpts, - rawSlug: string | string[], - versionFlag?: string, - force = false, + opts: GlobalOpts, + rawSlug: string | string[], + versionFlag?: string, + force = false ) { - if (Array.isArray(rawSlug)) { - let batchOpts = opts; + if (Array.isArray(rawSlug)) { + let batchOpts = opts; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + batchOpts = { + ...opts, + agent: picked.agent, + workdir: picked.workdir, + dir: picked.dir, + }; + } + } + + // Scope selection for batch install (copied from vercel-labs/skills) + if (batchOpts.agent && !batchOpts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(batchOpts.agent as AgentName); + if (scope === null) { + console.log('Installation cancelled'); + return; + } + if (scope) { + const workdir = resolveAgentWorkdir(batchOpts.agent as AgentName, true); + batchOpts = { + ...batchOpts, + workdir, + dir: `${workdir}/skills`, + globalScope: true, + globalScopeExplicit: true, + }; + } else { + batchOpts = { ...batchOpts, globalScope: false, globalScopeExplicit: true }; + } + } + + const results: { slug: string; status: 'ok' | 'fail' }[] = []; + for (const slug of rawSlug) { + try { + await cmdInstall(batchOpts, slug, versionFlag, force); + results.push({ slug, status: 'ok' }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.log(`✖ ${slug}: ${message}`); + results.push({ slug, status: 'fail' }); + } + } + const okCount = results.filter((r) => r.status === 'ok').length; + const failCount = results.filter((r) => r.status === 'fail').length; + console.log(`Summary: ${okCount} ok, ${failCount} fail`); + if (failCount > 0) { + process.exitCode = 1; + } + return; + } + + const trimmed = normalizeSkillSlugOrFail(rawSlug); + + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; if (!opts.agent && isInteractive()) { - const picked = await selectAgent(); - if (picked) { - batchOpts = { ...opts, agent: picked.agent, workdir: picked.workdir, dir: picked.dir }; - } + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } } - // Scope selection for batch install (copied from vercel-labs/skills) - if (batchOpts.agent && !batchOpts.globalScopeExplicit && isInteractive()) { - const scope = await selectScope(batchOpts.agent as AgentName); - if (scope === null) { - console.log("Installation cancelled"); - return; - } - if (scope) { - const workdir = resolveAgentWorkdir(batchOpts.agent as AgentName, true); - batchOpts = { ...batchOpts, workdir, dir: `${workdir}/skills`, globalScope: true, globalScopeExplicit: true }; - } else { - batchOpts = { ...batchOpts, globalScope: false, globalScopeExplicit: true }; - } - } - - const results: { slug: string; status: 'ok' | 'fail' }[] = []; - for (const slug of rawSlug) { - try { - await cmdInstall(batchOpts, slug, versionFlag, force); - results.push({ slug, status: 'ok' }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.log(`✖ ${slug}: ${message}`); - results.push({ slug, status: 'fail' }); - } - } - const okCount = results.filter((r) => r.status === 'ok').length; - const failCount = results.filter((r) => r.status === 'fail').length; - console.log(`Summary: ${okCount} ok, ${failCount} fail`); - if (failCount > 0) { - process.exitCode = 1; - } - return; - } - - const trimmed = normalizeSkillSlugOrFail(rawSlug); - - // Prompt for target agent when --agent is not provided and interactive - let installWorkdir = opts.workdir; - let installDir = opts.dir; - let installAgent = opts.agent; - if (!opts.agent && isInteractive()) { - const picked = await selectAgent(); - if (picked) { - installWorkdir = picked.workdir; - installDir = picked.dir; - installAgent = picked.agent; - } - } - - // Scope selection (copied from vercel-labs/skills) - if (installAgent && !opts.globalScopeExplicit && isInteractive()) { - const scope = await selectScope(installAgent as AgentName); - if (scope === null) { - console.log("Installation cancelled"); - return; - } - if (scope) { - installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); - installDir = `${installWorkdir}/skills`; - } - } - - const registry = await getRegistry(opts, { cache: true }); - await mkdir(installDir, { recursive: true }); - const target = join(installDir, trimmed); - - const lock = await readLockfile(installWorkdir); - const existingEntry = lock.skills[trimmed]; - if (isPinnedSkillEntry(existingEntry)) { - fail(`skill "${trimmed}" is pinned; run \`dt-skill unpin ${trimmed}\` first`); - } - - const spinner = createSpinner(`Resolving ${trimmed}`); - try { - // Fetch skill metadata including moderation status - const skillMeta = await apiRequest( - registry, - { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}` }, - ApiV1SkillResponseSchema, - ); + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log('Installation cancelled'); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } - // Check moderation status before proceeding - if (skillMeta.moderation?.isMalwareBlocked) { - spinner.fail(`Blocked: ${trimmed} is flagged as malicious`); - fail("This skill has been flagged as malware and cannot be installed."); - } - - if (skillMeta.skill && (skillMeta.skill as any).isPackage) { - spinner.stop(); - const children = (skillMeta.skill as any).children || []; - if (children.length === 0) { - fail(`Skill package "${trimmed}" has no children skills.`); - } - - // Prepare items for searchMultiselect - const items = children.map((c: any) => ({ - value: c.slug, - label: c.displayName || c.slug, - hint: c.summary || undefined, - })); - - const selectedSlugs = await searchMultiselect({ - message: `Select skills from package "${trimmed}" to install:`, - items, - required: true, - }); - - if (selectedSlugs === cancelSymbol || !Array.isArray(selectedSlugs) || selectedSlugs.length === 0) { - console.log("Installation cancelled"); - return; - } + const registry = await getRegistry(opts, { cache: true }); + await mkdir(installDir, { recursive: true }); + const target = join(installDir, trimmed); - // Install each selected sub-skill - for (const subSlug of selectedSlugs as string[]) { - const subSkill = children.find((c: any) => c.slug === subSlug); - const subVersion = String(subSkill?.version || "") || "latest"; - const subTarget = join(installDir, subSlug); + const lock = await readLockfile(installWorkdir); + const existingEntry = lock.skills[trimmed]; + if (isPinnedSkillEntry(existingEntry)) { + fail(`skill "${trimmed}" is pinned; run \`dt-skill unpin ${trimmed}\` first`); + } + + const spinner = createSpinner(`Resolving ${trimmed}`); + try { + // Fetch skill metadata including moderation status + const skillMeta = await apiRequest( + registry, + { method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}` }, + ApiV1SkillResponseSchema + ); + + // Check moderation status before proceeding + if (skillMeta.moderation?.isMalwareBlocked) { + spinner.fail(`Blocked: ${trimmed} is flagged as malicious`); + fail('This skill has been flagged as malware and cannot be installed.'); + } + + if (skillMeta.skill && (skillMeta.skill as any).isPackage) { + spinner.stop(); + const children = (skillMeta.skill as any).children || []; + if (children.length === 0) { + fail(`Skill package "${trimmed}" has no children skills.`); + } + + // Prepare items for searchMultiselect + const items = children.map((c: any) => ({ + value: c.slug, + label: c.displayName || c.slug, + hint: c.summary || undefined, + })); + + const selectedSlugs = await searchMultiselect({ + message: `Select skills from package "${trimmed}" to install:`, + items, + required: true, + }); + + if ( + selectedSlugs === cancelSymbol || + !Array.isArray(selectedSlugs) || + selectedSlugs.length === 0 + ) { + console.log('Installation cancelled'); + return; + } + + // Install each selected sub-skill + for (const subSlug of selectedSlugs as string[]) { + const subSkill = children.find((c: any) => c.slug === subSlug); + const subVersion = String(subSkill?.version || '') || 'latest'; + const subTarget = join(installDir, subSlug); + + if (!force) { + const exists = await fileExists(subTarget); + if (exists) { + console.log( + `Already installed: ${subTarget} (skipping, use --force to overwrite)` + ); + continue; + } + } else { + await rm(subTarget, { recursive: true, force: true }); + } + + const subSpinner = createSpinner(`Downloading sub-skill ${subSlug}@${subVersion}`); + try { + const zip = await downloadZip(registry, { + slug: subSlug, + version: subVersion, + }); + await extractZipToDir(zip, subTarget); + const installedFiles = await listTextFiles(subTarget); + const installedFingerprint = + installedFiles.length > 0 + ? hashSkillFiles(installedFiles).fingerprint + : undefined; + + await writeSkillOrigin(subTarget, { + version: 1, + registry, + slug: subSlug, + installedVersion: subVersion, + installedAt: Date.now(), + fingerprint: installedFingerprint, + }); + + lock.skills[subSlug] = withPinnedMetadata( + subVersion, + Date.now(), + lock.skills[subSlug] + ); + await writeLockfile(installWorkdir, lock); + const agentSuffix2 = installAgent + ? ` (${getAgentLabel(installAgent as import('../agents.js').AgentName)})` + : ''; + subSpinner.succeed( + `OK. Installed sub-skill ${subSlug} -> ${subTarget}${agentSuffix2}` + ); + } catch (err) { + subSpinner.fail(`Failed to install sub-skill ${subSlug}: ${formatError(err)}`); + throw err; + } + } + return; + } if (!force) { - const exists = await fileExists(subTarget); - if (exists) { - console.log(`Already installed: ${subTarget} (skipping, use --force to overwrite)`); - continue; - } - } else { - await rm(subTarget, { recursive: true, force: true }); + const exists = await fileExists(target); + if (exists) fail(`Already installed: ${target} (use --force)`); } - const subSpinner = createSpinner(`Downloading sub-skill ${subSlug}@${subVersion}`); - try { - const zip = await downloadZip(registry, { - slug: subSlug, - version: subVersion, - }); - await extractZipToDir(zip, subTarget); - const installedFiles = await listTextFiles(subTarget); - const installedFingerprint = + if (skillMeta.moderation?.isSuspicious && !force) { + spinner.stop(); + console.log( + `\n⚠️ Warning: "${trimmed}" is flagged for ClawHub security review.\n` + + ' This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n' + + ' Review the skill code before use.\n' + ); + if (isInteractive()) { + const confirm = await promptConfirm('Install anyway?'); + if (!confirm) fail('Installation cancelled'); + spinner.start(`Resolving ${trimmed}`); + } else { + fail('Use --force to install suspicious skills in non-interactive mode'); + } + } + + const resolvedVersion = versionFlag ?? skillMeta.latestVersion?.version ?? 'latest'; + + if (versionFlag) { + await apiRequest( + registry, + { + method: 'GET', + path: `${ApiRoutes.skills}/${encodeURIComponent( + trimmed + )}/versions/${encodeURIComponent(resolvedVersion)}`, + }, + ApiV1SkillVersionResponseSchema + ); + } + + if (force) { + await rm(target, { recursive: true, force: true }); + } + + spinner.text = `Downloading ${trimmed}@${resolvedVersion}`; + const zip = await downloadZip(registry, { + slug: trimmed, + version: resolvedVersion, + }); + await extractZipToDir(zip, target); + const installedFiles = await listTextFiles(target); + const installedFingerprint = installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; - await writeSkillOrigin(subTarget, { + await writeSkillOrigin(target, { version: 1, registry, - slug: subSlug, - installedVersion: subVersion, + slug: trimmed, + installedVersion: resolvedVersion, installedAt: Date.now(), fingerprint: installedFingerprint, - }); + }); + + lock.skills[trimmed] = withPinnedMetadata(resolvedVersion, Date.now(), existingEntry); + await writeLockfile(installWorkdir, lock); + const agentSuffix = installAgent + ? ` (${getAgentLabel(installAgent as import('../agents.js').AgentName)})` + : ''; + spinner.succeed(`OK. Installed ${trimmed} -> ${target}${agentSuffix}`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } +} - lock.skills[subSlug] = withPinnedMetadata(subVersion, Date.now(), lock.skills[subSlug]); - await writeLockfile(installWorkdir, lock); - const agentSuffix2 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; - subSpinner.succeed(`OK. Installed sub-skill ${subSlug} -> ${subTarget}${agentSuffix2}`); - } catch (err) { - subSpinner.fail(`Failed to install sub-skill ${subSlug}: ${formatError(err)}`); - throw err; +export async function cmdUpdate( + opts: GlobalOpts, + slugArg: string | undefined, + options: { all?: boolean; version?: string; force?: boolean }, + inputAllowed: boolean +) { + const slug = slugArg ? normalizeSkillSlugOrFail(slugArg) : undefined; + const all = Boolean(options.all); + if (!slug && !all) fail('Provide or --all'); + if (slug && all) fail('Use either or --all'); + if (options.version && !slug) fail('--version requires a single '); + if (options.version && !semver.valid(options.version)) fail('--version must be valid semver'); + + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; } - } - return; } - if (!force) { - const exists = await fileExists(target); - if (exists) fail(`Already installed: ${target} (use --force)`); + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log('Update cancelled'); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } } - if (skillMeta.moderation?.isSuspicious && !force) { - spinner.stop(); - console.log( - `\n⚠️ Warning: "${trimmed}" is flagged for ClawHub security review.\n` + - " This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n" + - " Review the skill code before use.\n", - ); - if (isInteractive()) { - const confirm = await promptConfirm("Install anyway?"); - if (!confirm) fail("Installation cancelled"); - spinner.start(`Resolving ${trimmed}`); - } else { - fail("Use --force to install suspicious skills in non-interactive mode"); - } + const lock = await readLockfile(installWorkdir); + if (slug && isPinnedSkillEntry(lock.skills[slug])) { + fail(`skill "${slug}" is pinned; run \`dt-skill unpin ${slug}\` first`); + } + const allowPrompt = isInteractive() && inputAllowed; + + const registry = await getRegistry(opts, { cache: true }); + const requestedSlugs = slug ? [slug] : Object.keys(lock.skills).filter(isSafeSkillSlug); + const skippedPinned = slug + ? [] + : requestedSlugs.filter((entry) => isPinnedSkillEntry(lock.skills[entry])); + const slugs = slug + ? requestedSlugs + : requestedSlugs.filter((entry) => !isPinnedSkillEntry(lock.skills[entry])); + if (slugs.length === 0) { + if (skippedPinned.length > 0) { + const suffix = skippedPinned.length === 1 ? '' : 's'; + console.log( + `Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(', ')}` + ); + return; + } + console.log('No installed skills.'); + return; } - const resolvedVersion = versionFlag ?? skillMeta.latestVersion?.version ?? "latest"; + for (const entry of slugs) { + const spinner = createSpinner(`Checking ${entry}`); + try { + const target = join(installDir, entry); + const exists = await fileExists(target); + const existingOrigin = exists ? await readSkillOrigin(target) : null; + + // Always fetch skill metadata to check moderation status + const skillMeta = await apiRequest( + registry, + { method: 'GET', path: `${ApiRoutes.skills}/${encodeURIComponent(entry)}` }, + ApiV1SkillResponseSchema + ); + + // Check moderation status before proceeding + if (skillMeta.moderation?.isMalwareBlocked) { + spinner.fail(`${entry}: blocked as malicious`); + console.log(' This skill has been flagged as malware and cannot be updated.'); + continue; + } + + if (skillMeta.moderation?.isSuspicious && !options.force) { + spinner.stop(); + console.log( + `\n⚠️ Warning: "${entry}" is flagged for ClawHub security review.\n` + + ' This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n' + ); + if (allowPrompt) { + const confirm = await promptConfirm('Update anyway?'); + if (!confirm) { + console.log(`${entry}: skipped`); + continue; + } + spinner.start(`Checking ${entry}`); + } else { + console.log(`${entry}: skipped (use --force to update suspicious skills)`); + continue; + } + } + + let localFingerprint: string | null = null; + if (exists) { + const filesOnDisk = await listTextFiles(target); + if (filesOnDisk.length > 0) { + const hashed = hashSkillFiles(filesOnDisk); + localFingerprint = hashed.fingerprint; + } + } + + let resolveResult: ResolveResult; + if (localFingerprint) { + resolveResult = await resolveSkillVersion(registry, entry, localFingerprint); + } else { + resolveResult = { match: null, latestVersion: skillMeta.latestVersion ?? null }; + } + + const latest = resolveResult.latestVersion?.version ?? null; + const matched = + resolveResult.match?.version ?? + (localFingerprint && + existingOrigin?.fingerprint === localFingerprint && + existingOrigin.slug === entry + ? existingOrigin.installedVersion + : null); + + if (matched && lock.skills[entry]?.version !== matched) { + lock.skills[entry] = withPinnedMetadata( + matched, + lock.skills[entry]?.installedAt ?? Date.now(), + lock.skills[entry] + ); + } + + if (!latest) { + spinner.fail(`${entry}: not found`); + continue; + } + + if (!matched && localFingerprint && !options.force) { + spinner.stop(); + if (!allowPrompt) { + console.log(`${entry}: local changes (no match). Use --force to overwrite.`); + continue; + } + const confirm = await promptConfirm( + `${entry}: local changes (no match). Overwrite with ${ + options.version ?? latest + }?` + ); + if (!confirm) { + console.log(`${entry}: skipped`); + continue; + } + spinner.start(`Updating ${entry} -> ${options.version ?? latest}`); + } + + const targetVersion = options.version ?? latest; + if (options.version) { + if (matched && matched === targetVersion) { + spinner.succeed(`${entry}: already at ${matched}`); + continue; + } + } else if (matched && semver.valid(matched) && semver.gte(matched, targetVersion)) { + spinner.succeed(`${entry}: up to date (${matched})`); + continue; + } + + if (spinner.isSpinning) { + spinner.text = `Updating ${entry} -> ${targetVersion}`; + } else { + spinner.start(`Updating ${entry} -> ${targetVersion}`); + } + const zip = await downloadZip(registry, { + slug: entry, + version: targetVersion, + }); + const preparedDir = await prepareSkillUpdate(zip, target); + + try { + const installedFiles = await listTextFiles(preparedDir); + const installedFingerprint = + installedFiles.length > 0 + ? hashSkillFiles(installedFiles).fingerprint + : undefined; + await writeSkillOrigin(preparedDir, { + version: 1, + registry: existingOrigin?.registry ?? registry, + slug: existingOrigin?.slug ?? entry, + installedVersion: targetVersion, + installedAt: existingOrigin?.installedAt ?? Date.now(), + fingerprint: installedFingerprint, + }); + await replaceSkillDirectory(preparedDir, target, exists); + } catch (error) { + await rm(preparedDir, { recursive: true, force: true }).catch(() => {}); + throw error; + } + + lock.skills[entry] = withPinnedMetadata(targetVersion, Date.now(), lock.skills[entry]); + const agentSuffix3 = installAgent + ? ` (${getAgentLabel(installAgent as import('../agents.js').AgentName)})` + : ''; + spinner.succeed(`${entry}: updated -> ${targetVersion}${agentSuffix3}`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } + } - if (versionFlag) { - await apiRequest( - registry, - { - method: "GET", - path: `${ApiRoutes.skills}/${encodeURIComponent(trimmed)}/versions/${encodeURIComponent( - resolvedVersion, - )}`, - }, - ApiV1SkillVersionResponseSchema, - ); - } - - if (force) { - await rm(target, { recursive: true, force: true }); - } - - spinner.text = `Downloading ${trimmed}@${resolvedVersion}`; - const zip = await downloadZip(registry, { - slug: trimmed, - version: resolvedVersion, - }); - await extractZipToDir(zip, target); - const installedFiles = await listTextFiles(target); - const installedFingerprint = - installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; - - await writeSkillOrigin(target, { - version: 1, - registry, - slug: trimmed, - installedVersion: resolvedVersion, - installedAt: Date.now(), - fingerprint: installedFingerprint, - }); - - lock.skills[trimmed] = withPinnedMetadata(resolvedVersion, Date.now(), existingEntry); await writeLockfile(installWorkdir, lock); - const agentSuffix = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; - spinner.succeed(`OK. Installed ${trimmed} -> ${target}${agentSuffix}`); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } -} - -export async function cmdUpdate( - opts: GlobalOpts, - slugArg: string | undefined, - options: { all?: boolean; version?: string; force?: boolean }, - inputAllowed: boolean, -) { - const slug = slugArg ? normalizeSkillSlugOrFail(slugArg) : undefined; - const all = Boolean(options.all); - if (!slug && !all) fail("Provide or --all"); - if (slug && all) fail("Use either or --all"); - if (options.version && !slug) fail("--version requires a single "); - if (options.version && !semver.valid(options.version)) fail("--version must be valid semver"); - - // Prompt for target agent when --agent is not provided and interactive - let installWorkdir = opts.workdir; - let installDir = opts.dir; - let installAgent = opts.agent; - if (!opts.agent && isInteractive()) { - const picked = await selectAgent(); - if (picked) { - installWorkdir = picked.workdir; - installDir = picked.dir; - installAgent = picked.agent; - } - } - - // Scope selection (copied from vercel-labs/skills) - if (installAgent && !opts.globalScopeExplicit && isInteractive()) { - const scope = await selectScope(installAgent as AgentName); - if (scope === null) { - console.log("Update cancelled"); - return; - } - if (scope) { - installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); - installDir = `${installWorkdir}/skills`; - } - } - - const lock = await readLockfile(installWorkdir); - if (slug && isPinnedSkillEntry(lock.skills[slug])) { - fail(`skill "${slug}" is pinned; run \`dt-skill unpin ${slug}\` first`); - } - const allowPrompt = isInteractive() && inputAllowed; - - const registry = await getRegistry(opts, { cache: true }); - const requestedSlugs = slug ? [slug] : Object.keys(lock.skills).filter(isSafeSkillSlug); - const skippedPinned = slug - ? [] - : requestedSlugs.filter((entry) => isPinnedSkillEntry(lock.skills[entry])); - const slugs = slug - ? requestedSlugs - : requestedSlugs.filter((entry) => !isPinnedSkillEntry(lock.skills[entry])); - if (slugs.length === 0) { if (skippedPinned.length > 0) { - const suffix = skippedPinned.length === 1 ? "" : "s"; - console.log( - `Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(", ")}`, - ); - return; - } - console.log("No installed skills."); - return; - } - - for (const entry of slugs) { - const spinner = createSpinner(`Checking ${entry}`); - try { - const target = join(installDir, entry); - const exists = await fileExists(target); - const existingOrigin = exists ? await readSkillOrigin(target) : null; - - // Always fetch skill metadata to check moderation status - const skillMeta = await apiRequest( - registry, - { method: "GET", path: `${ApiRoutes.skills}/${encodeURIComponent(entry)}` }, - ApiV1SkillResponseSchema, - ); - - // Check moderation status before proceeding - if (skillMeta.moderation?.isMalwareBlocked) { - spinner.fail(`${entry}: blocked as malicious`); - console.log(" This skill has been flagged as malware and cannot be updated."); - continue; - } - - if (skillMeta.moderation?.isSuspicious && !options.force) { - spinner.stop(); + const suffix = skippedPinned.length === 1 ? '' : 's'; console.log( - `\n⚠️ Warning: "${entry}" is flagged for ClawHub security review.\n` + - " This skill may contain risky patterns (crypto keys, external APIs, eval, etc.)\n", - ); - if (allowPrompt) { - const confirm = await promptConfirm("Update anyway?"); - if (!confirm) { - console.log(`${entry}: skipped`); - continue; - } - spinner.start(`Checking ${entry}`); - } else { - console.log(`${entry}: skipped (use --force to update suspicious skills)`); - continue; - } - } - - let localFingerprint: string | null = null; - if (exists) { - const filesOnDisk = await listTextFiles(target); - if (filesOnDisk.length > 0) { - const hashed = hashSkillFiles(filesOnDisk); - localFingerprint = hashed.fingerprint; - } - } - - let resolveResult: ResolveResult; - if (localFingerprint) { - resolveResult = await resolveSkillVersion(registry, entry, localFingerprint); - } else { - resolveResult = { match: null, latestVersion: skillMeta.latestVersion ?? null }; - } - - const latest = resolveResult.latestVersion?.version ?? null; - const matched = - resolveResult.match?.version ?? - (localFingerprint && - existingOrigin?.fingerprint === localFingerprint && - existingOrigin.slug === entry - ? existingOrigin.installedVersion - : null); - - if (matched && lock.skills[entry]?.version !== matched) { - lock.skills[entry] = withPinnedMetadata( - matched, - lock.skills[entry]?.installedAt ?? Date.now(), - lock.skills[entry], - ); - } - - if (!latest) { - spinner.fail(`${entry}: not found`); - continue; - } - - if (!matched && localFingerprint && !options.force) { - spinner.stop(); - if (!allowPrompt) { - console.log(`${entry}: local changes (no match). Use --force to overwrite.`); - continue; - } - const confirm = await promptConfirm( - `${entry}: local changes (no match). Overwrite with ${options.version ?? latest}?`, + `Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(', ')}` ); - if (!confirm) { - console.log(`${entry}: skipped`); - continue; - } - spinner.start(`Updating ${entry} -> ${options.version ?? latest}`); - } - - const targetVersion = options.version ?? latest; - if (options.version) { - if (matched && matched === targetVersion) { - spinner.succeed(`${entry}: already at ${matched}`); - continue; - } - } else if (matched && semver.valid(matched) && semver.gte(matched, targetVersion)) { - spinner.succeed(`${entry}: up to date (${matched})`); - continue; - } - - if (spinner.isSpinning) { - spinner.text = `Updating ${entry} -> ${targetVersion}`; - } else { - spinner.start(`Updating ${entry} -> ${targetVersion}`); - } - const zip = await downloadZip(registry, { - slug: entry, - version: targetVersion, - }); - const preparedDir = await prepareSkillUpdate(zip, target); - - try { - const installedFiles = await listTextFiles(preparedDir); - const installedFingerprint = - installedFiles.length > 0 ? hashSkillFiles(installedFiles).fingerprint : undefined; - await writeSkillOrigin(preparedDir, { - version: 1, - registry: existingOrigin?.registry ?? registry, - slug: existingOrigin?.slug ?? entry, - installedVersion: targetVersion, - installedAt: existingOrigin?.installedAt ?? Date.now(), - fingerprint: installedFingerprint, - }); - await replaceSkillDirectory(preparedDir, target, exists); - } catch (error) { - await rm(preparedDir, { recursive: true, force: true }).catch(() => {}); - throw error; - } - - lock.skills[entry] = withPinnedMetadata(targetVersion, Date.now(), lock.skills[entry]); - const agentSuffix3 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; - spinner.succeed(`${entry}: updated -> ${targetVersion}${agentSuffix3}`); - } catch (error) { - spinner.fail(formatError(error)); - throw error; } - } - - await writeLockfile(installWorkdir, lock); - if (skippedPinned.length > 0) { - const suffix = skippedPinned.length === 1 ? "" : "s"; - console.log( - `Skipped ${skippedPinned.length} pinned skill${suffix}: ${skippedPinned.join(", ")}`, - ); - } } async function prepareSkillUpdate(zip: Uint8Array, target: string) { - await mkdir(dirname(target), { recursive: true }); - const preparedDir = await mkdtemp(join(dirname(target), `.${basename(target)}-update-`)); - try { - await extractZipToDir(zip, preparedDir); - return preparedDir; - } catch (error) { - await rm(preparedDir, { recursive: true, force: true }).catch(() => {}); - throw error; - } + await mkdir(dirname(target), { recursive: true }); + const preparedDir = await mkdtemp(join(dirname(target), `.${basename(target)}-update-`)); + try { + await extractZipToDir(zip, preparedDir); + return preparedDir; + } catch (error) { + await rm(preparedDir, { recursive: true, force: true }).catch(() => {}); + throw error; + } } async function replaceSkillDirectory(preparedDir: string, target: string, targetExists: boolean) { - const backupDir = `${preparedDir}-previous`; - let movedExisting = false; + const backupDir = `${preparedDir}-previous`; + let movedExisting = false; - try { - if (targetExists) { - await rename(target, backupDir); - movedExisting = true; + try { + if (targetExists) { + await rename(target, backupDir); + movedExisting = true; + } + await rename(preparedDir, target); + } catch (error) { + if (movedExisting) { + try { + await rename(backupDir, target); + } catch (rollbackError) { + throw new AggregateError( + [error, rollbackError], + `Failed to replace ${target} and restore the previous installation` + ); + } + } + throw error; } - await rename(preparedDir, target); - } catch (error) { + if (movedExisting) { - try { - await rename(backupDir, target); - } catch (rollbackError) { - throw new AggregateError( - [error, rollbackError], - `Failed to replace ${target} and restore the previous installation`, - ); - } + await rm(backupDir, { recursive: true, force: true }).catch(() => {}); } - throw error; - } - - if (movedExisting) { - await rm(backupDir, { recursive: true, force: true }).catch(() => {}); - } } export async function cmdList(opts: GlobalOpts) { - // Prompt for target agent when --agent is not provided and interactive - let installWorkdir = opts.workdir; - let installDir = opts.dir; - let installAgent = opts.agent; - if (!opts.agent && isInteractive()) { - const picked = await selectAgent(); - if (picked) { - installWorkdir = picked.workdir; - installDir = picked.dir; - installAgent = picked.agent; - } - } - - // Scope selection (copied from vercel-labs/skills) - if (installAgent && !opts.globalScopeExplicit && isInteractive()) { - const scope = await selectScope(installAgent as AgentName); - if (scope === null) { - console.log("List cancelled"); - return; - } - if (scope) { - installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); - installDir = `${installWorkdir}/skills`; - } - } - - const lock = await readLockfile(installWorkdir); - const entries = Object.entries(lock.skills); - const manualSkills = await listManualSkills(installDir, new Set(Object.keys(lock.skills))); - if (installAgent) { - console.log(`Skills for ${getAgentLabel(installAgent as import("../agents.js").AgentName)} (${installDir}):`); - } - if (entries.length === 0 && manualSkills.length === 0) { - console.log("No installed skills."); - return; - } - for (const [slug, entry] of entries) { - const e = entry as { version?: string | null; pinned?: boolean; pinReason?: string }; - const pinned = isPinnedSkillEntry(e) ? ` pinned${formatPinnedDetails(e)}` : ""; - console.log(`${slug} ${e.version ?? "latest"}${pinned}`); - } - if (manualSkills.length > 0) { - if (entries.length > 0) console.log(); - console.log("Manually installed (not tracked by dt-skill):"); - for (const slug of manualSkills) { - console.log(` ${slug}`); - } - } + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } + } + + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log('List cancelled'); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } + + const lock = await readLockfile(installWorkdir); + const entries = Object.entries(lock.skills); + const manualSkills = await listManualSkills(installDir, new Set(Object.keys(lock.skills))); + if (installAgent) { + console.log( + `Skills for ${getAgentLabel( + installAgent as import('../agents.js').AgentName + )} (${installDir}):` + ); + } + if (entries.length === 0 && manualSkills.length === 0) { + console.log('No installed skills.'); + return; + } + for (const [slug, entry] of entries) { + const e = entry as { version?: string | null; pinned?: boolean; pinReason?: string }; + const pinned = isPinnedSkillEntry(e) ? ` pinned${formatPinnedDetails(e)}` : ''; + console.log(`${slug} ${e.version ?? 'latest'}${pinned}`); + } + if (manualSkills.length > 0) { + if (entries.length > 0) console.log(); + console.log('Manually installed (not tracked by dt-skill):'); + for (const slug of manualSkills) { + console.log(` ${slug}`); + } + } } export async function cmdPin(opts: GlobalOpts, slug: string, options: { reason?: string } = {}) { - const trimmed = normalizeSkillSlugOrFail(slug); - const lock = await readLockfile(opts.workdir); - const existing = lock.skills[trimmed]; - if (!existing) fail(`Not installed: ${trimmed}`); - - const reason = options.reason?.trim() || existing.pinReason; - if (isPinnedSkillEntry(existing) && reason === existing.pinReason) { - console.log(`Skill "${trimmed}" is already pinned${reason ? `: ${reason}` : ""}`); - return; - } - - lock.skills[trimmed] = { - ...existing, - pinned: true, - ...(reason ? { pinReason: reason } : {}), - }; - await writeLockfile(opts.workdir, lock); - console.log(`Pinned ${trimmed}${reason ? `: ${reason}` : ""}`); + const trimmed = normalizeSkillSlugOrFail(slug); + const lock = await readLockfile(opts.workdir); + const existing = lock.skills[trimmed]; + if (!existing) fail(`Not installed: ${trimmed}`); + + const reason = options.reason?.trim() || existing.pinReason; + if (isPinnedSkillEntry(existing) && reason === existing.pinReason) { + console.log(`Skill "${trimmed}" is already pinned${reason ? `: ${reason}` : ''}`); + return; + } + + lock.skills[trimmed] = { + ...existing, + pinned: true, + ...(reason ? { pinReason: reason } : {}), + }; + await writeLockfile(opts.workdir, lock); + console.log(`Pinned ${trimmed}${reason ? `: ${reason}` : ''}`); } export async function cmdUnpin(opts: GlobalOpts, slug: string) { - const trimmed = normalizeSkillSlugOrFail(slug); - const lock = await readLockfile(opts.workdir); - const existing = lock.skills[trimmed]; - if (!existing) fail(`Not installed: ${trimmed}`); - if (!isPinnedSkillEntry(existing)) fail(`Skill "${trimmed}" is not pinned`); - - lock.skills[trimmed] = { - version: existing.version, - installedAt: existing.installedAt, - }; - await writeLockfile(opts.workdir, lock); - console.log(`Unpinned ${trimmed}`); + const trimmed = normalizeSkillSlugOrFail(slug); + const lock = await readLockfile(opts.workdir); + const existing = lock.skills[trimmed]; + if (!existing) fail(`Not installed: ${trimmed}`); + if (!isPinnedSkillEntry(existing)) fail(`Skill "${trimmed}" is not pinned`); + + lock.skills[trimmed] = { + version: existing.version, + installedAt: existing.installedAt, + }; + await writeLockfile(opts.workdir, lock); + console.log(`Unpinned ${trimmed}`); } export async function cmdUninstall( - opts: GlobalOpts, - slug: string, - options: { yes?: boolean } = {}, - inputAllowed: boolean, + opts: GlobalOpts, + slug: string, + options: { yes?: boolean } = {}, + inputAllowed: boolean ) { - const trimmed = normalizeSkillSlugOrFail(slug); - - // Prompt for target agent when --agent is not provided and interactive - let installWorkdir = opts.workdir; - let installDir = opts.dir; - let installAgent = opts.agent; - if (!opts.agent && isInteractive()) { - const picked = await selectAgent(); - if (picked) { - installWorkdir = picked.workdir; - installDir = picked.dir; - installAgent = picked.agent; - } - } - - // Scope selection (copied from vercel-labs/skills) - if (installAgent && !opts.globalScopeExplicit && isInteractive()) { - const scope = await selectScope(installAgent as AgentName); - if (scope === null) { - console.log("Uninstall cancelled"); - return; - } - if (scope) { - installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); - installDir = `${installWorkdir}/skills`; - } - } - - const lock = await readLockfile(installWorkdir); - if (!lock.skills[trimmed]) { - fail(`Not installed: ${trimmed}`); - } - - const allowPrompt = isInteractive() && inputAllowed; - if (!options.yes) { - if (!allowPrompt) fail("Pass --yes (no input)"); - const confirm = await promptConfirm(`Uninstall ${trimmed}?`); - if (!confirm) { - console.log("Cancelled."); - return; - } - } - - const spinner = createSpinner(`Uninstalling ${trimmed}`); - try { - const target = join(installDir, trimmed); + const trimmed = normalizeSkillSlugOrFail(slug); - await rm(target, { recursive: true, force: true }); + // Prompt for target agent when --agent is not provided and interactive + let installWorkdir = opts.workdir; + let installDir = opts.dir; + let installAgent = opts.agent; + if (!opts.agent && isInteractive()) { + const picked = await selectAgent(); + if (picked) { + installWorkdir = picked.workdir; + installDir = picked.dir; + installAgent = picked.agent; + } + } - delete lock.skills[trimmed]; - await writeLockfile(installWorkdir, lock); + // Scope selection (copied from vercel-labs/skills) + if (installAgent && !opts.globalScopeExplicit && isInteractive()) { + const scope = await selectScope(installAgent as AgentName); + if (scope === null) { + console.log('Uninstall cancelled'); + return; + } + if (scope) { + installWorkdir = resolveAgentWorkdir(installAgent as AgentName, true); + installDir = `${installWorkdir}/skills`; + } + } + + const lock = await readLockfile(installWorkdir); + if (!lock.skills[trimmed]) { + fail(`Not installed: ${trimmed}`); + } - const agentSuffix4 = installAgent ? ` (${getAgentLabel(installAgent as import("../agents.js").AgentName)})` : ""; - spinner.succeed(`Uninstalled ${trimmed}${agentSuffix4}`); - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + const allowPrompt = isInteractive() && inputAllowed; + if (!options.yes) { + if (!allowPrompt) fail('Pass --yes (no input)'); + const confirm = await promptConfirm(`Uninstall ${trimmed}?`); + if (!confirm) { + console.log('Cancelled.'); + return; + } + } + + const spinner = createSpinner(`Uninstalling ${trimmed}`); + try { + const target = join(installDir, trimmed); + + await rm(target, { recursive: true, force: true }); + + delete lock.skills[trimmed]; + await writeLockfile(installWorkdir, lock); + + const agentSuffix4 = installAgent + ? ` (${getAgentLabel(installAgent as import('../agents.js').AgentName)})` + : ''; + spinner.succeed(`Uninstalled ${trimmed}${agentSuffix4}`); + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } -type ExploreSort = "newest" | "downloads" | "rating" | "installs" | "installsAllTime" | "trending"; +type ExploreSort = 'newest' | 'downloads' | 'rating' | 'installs' | 'installsAllTime' | 'trending'; type ApiExploreSort = - | "createdAt" - | "updated" - | "downloads" - | "stars" - | "installsCurrent" - | "installsAllTime" - | "trending"; + | 'createdAt' + | 'updated' + | 'downloads' + | 'stars' + | 'installsCurrent' + | 'installsAllTime' + | 'trending'; export async function cmdExplore( - opts: GlobalOpts, - options: { limit?: number; sort?: string; json?: boolean } = {}, + opts: GlobalOpts, + options: { limit?: number; sort?: string; json?: boolean } = {} ) { - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner("Fetching latest skills"); - try { - const url = registryUrl(ApiRoutes.skills, registry); - const boundedLimit = clampLimit(options.limit ?? 25); - const { apiSort } = resolveExploreSort(options.sort); - url.searchParams.set("limit", String(boundedLimit)); - if (apiSort !== "updated") url.searchParams.set("sort", apiSort); - const result = await apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1SkillListResponseSchema, - ); + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner('Fetching latest skills'); + try { + const url = registryUrl(ApiRoutes.skills, registry); + const boundedLimit = clampLimit(options.limit ?? 25); + const { apiSort } = resolveExploreSort(options.sort); + url.searchParams.set('limit', String(boundedLimit)); + if (apiSort !== 'updated') url.searchParams.set('sort', apiSort); + const result = await apiRequest( + registry, + { method: 'GET', url: url.toString() }, + ApiV1SkillListResponseSchema + ); - spinner.stop(); - if (options.json) { - console.log(JSON.stringify(result, null, 2)); - return; - } - if (result.items.length === 0) { - console.log("No skills found."); - return; - } + spinner.stop(); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + return; + } + if (result.items.length === 0) { + console.log('No skills found.'); + return; + } - for (const item of result.items) { - console.log(formatExploreLine(item)); + for (const item of result.items) { + console.log(formatExploreLine(item)); + } + } catch (error) { + spinner.fail(formatError(error)); + throw error; } - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } } export function formatExploreLine(item: { - slug: string; - summary?: string | null; - updatedAt: number; - latestVersion?: { version: string } | null; + slug: string; + summary?: string | null; + updatedAt: number; + latestVersion?: { version: string } | null; }) { - const version = item.latestVersion?.version ?? "?"; - const age = formatRelativeTime(item.updatedAt); - const summary = item.summary ? ` ${truncate(item.summary, 50)}` : ""; - return `${item.slug} v${version} ${age}${summary}`; + const version = item.latestVersion?.version ?? '?'; + const age = formatRelativeTime(item.updatedAt); + const summary = item.summary ? ` ${truncate(item.summary, 50)}` : ''; + return `${item.slug} v${version} ${age}${summary}`; } export function clampLimit(limit: number, fallback = 25) { - if (!Number.isFinite(limit)) return fallback; - return Math.min(Math.max(1, limit), 200); + if (!Number.isFinite(limit)) return fallback; + return Math.min(Math.max(1, limit), 200); } function formatRelativeTime(timestamp: number): string { - const now = Date.now(); - const diff = now - timestamp; - const seconds = Math.floor(diff / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 30) { - const months = Math.floor(days / 30); - return `${months}mo ago`; - } - if (days > 0) return `${days}d ago`; - if (hours > 0) return `${hours}h ago`; - if (minutes > 0) return `${minutes}m ago`; - return "just now"; + const now = Date.now(); + const diff = now - timestamp; + const seconds = Math.floor(diff / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 30) { + const months = Math.floor(days / 30); + return `${months}mo ago`; + } + if (days > 0) return `${days}d ago`; + if (hours > 0) return `${hours}h ago`; + if (minutes > 0) return `${minutes}m ago`; + return 'just now'; } function truncate(str: string, maxLen: number): string { - if (str.length <= maxLen) return str; - return `${str.slice(0, maxLen - 1)}…`; + if (str.length <= maxLen) return str; + return `${str.slice(0, maxLen - 1)}…`; } function resolveExploreSort(raw?: string): { sort: ExploreSort; apiSort: ApiExploreSort } { - const normalized = raw?.trim().toLowerCase(); - if ( - !normalized || - normalized === "newest" || - normalized === "createdat" || - normalized === "created-at" - ) { - return { sort: "newest", apiSort: "createdAt" }; - } - if (normalized === "updated") { - return { sort: "newest", apiSort: "updated" }; - } - if (normalized === "downloads" || normalized === "download") { - return { sort: "downloads", apiSort: "downloads" }; - } - if (normalized === "rating" || normalized === "stars" || normalized === "star") { - return { sort: "rating", apiSort: "stars" }; - } - if ( - normalized === "installs" || - normalized === "install" || - normalized === "installscurrent" || - normalized === "installs-current" || - normalized === "current" - ) { - return { sort: "installs", apiSort: "installsCurrent" }; - } - if (normalized === "installsalltime" || normalized === "installs-all-time") { - return { sort: "installsAllTime", apiSort: "installsAllTime" }; - } - if (normalized === "trending") { - return { sort: "trending", apiSort: "trending" }; - } - return fail( - `Invalid sort "${raw}". Use newest, updated, downloads, rating, installs, installsAllTime, or trending.`, - ); + const normalized = raw?.trim().toLowerCase(); + if ( + !normalized || + normalized === 'newest' || + normalized === 'createdat' || + normalized === 'created-at' + ) { + return { sort: 'newest', apiSort: 'createdAt' }; + } + if (normalized === 'updated') { + return { sort: 'newest', apiSort: 'updated' }; + } + if (normalized === 'downloads' || normalized === 'download') { + return { sort: 'downloads', apiSort: 'downloads' }; + } + if (normalized === 'rating' || normalized === 'stars' || normalized === 'star') { + return { sort: 'rating', apiSort: 'stars' }; + } + if ( + normalized === 'installs' || + normalized === 'install' || + normalized === 'installscurrent' || + normalized === 'installs-current' || + normalized === 'current' + ) { + return { sort: 'installs', apiSort: 'installsCurrent' }; + } + if (normalized === 'installsalltime' || normalized === 'installs-all-time') { + return { sort: 'installsAllTime', apiSort: 'installsAllTime' }; + } + if (normalized === 'trending') { + return { sort: 'trending', apiSort: 'trending' }; + } + return fail( + `Invalid sort "${raw}". Use newest, updated, downloads, rating, installs, installsAllTime, or trending.` + ); } -async function resolveSkillVersion(registry: string, slug: string, hash: string): Promise { - const url = registryUrl(ApiRoutes.resolve, registry); - url.searchParams.set("slug", slug); - url.searchParams.set("hash", hash); - return apiRequest( - registry, - { method: "GET", url: url.toString() }, - ApiV1SkillResolveResponseSchema, - ); +async function resolveSkillVersion( + registry: string, + slug: string, + hash: string +): Promise { + const url = registryUrl(ApiRoutes.resolve, registry); + url.searchParams.set('slug', slug); + url.searchParams.set('hash', hash); + return apiRequest( + registry, + { method: 'GET', url: url.toString() }, + ApiV1SkillResolveResponseSchema + ); } async function fileExists(path: string) { - try { - await stat(path); - return true; - } catch { - return false; - } + try { + await stat(path); + return true; + } catch { + return false; + } } diff --git a/dt-skill/src/cli/commands/star.ts b/dt-skill/src/cli/commands/star.ts index 95a596c8..5e96911b 100644 --- a/dt-skill/src/cli/commands/star.ts +++ b/dt-skill/src/cli/commands/star.ts @@ -1,37 +1,39 @@ -import { apiRequest } from "../../http.js"; -import { ApiRoutes, ApiV1StarResponseSchema } from "../../schema/index.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; +import { apiRequest } from '../../http.js'; +import { ApiRoutes, ApiV1StarResponseSchema } from '../../schema/index.js'; +import { getRegistry } from '../registry.js'; +import type { GlobalOpts } from '../types.js'; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'; export async function cmdStarSkill( - opts: GlobalOpts, - slugArg: string, - options: { yes?: boolean }, - inputAllowed: boolean, + opts: GlobalOpts, + slugArg: string, + options: { yes?: boolean }, + inputAllowed: boolean ) { - const slug = slugArg.trim().toLowerCase(); - if (!slug) fail("Slug required"); - const allowPrompt = isInteractive() && inputAllowed !== false; + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail('Slug required'); + const allowPrompt = isInteractive() && inputAllowed !== false; - if (!options.yes) { - if (!allowPrompt) fail("Pass --yes (no input)"); - const ok = await promptConfirm(`Star ${slug}?`); - if (!ok) return undefined; - } + if (!options.yes) { + if (!allowPrompt) fail('Pass --yes (no input)'); + const ok = await promptConfirm(`Star ${slug}?`); + if (!ok) return undefined; + } - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Starring ${slug}`); - try { - const result = await apiRequest( - registry, - { method: "POST", path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}` }, - ApiV1StarResponseSchema, - ); - spinner.succeed(result.alreadyStarred ? `OK. ${slug} already starred.` : `OK. Starred ${slug}`); - return result; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Starring ${slug}`); + try { + const result = await apiRequest( + registry, + { method: 'POST', path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}` }, + ApiV1StarResponseSchema + ); + spinner.succeed( + result.alreadyStarred ? `OK. ${slug} already starred.` : `OK. Starred ${slug}` + ); + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } diff --git a/dt-skill/src/cli/commands/unstar.ts b/dt-skill/src/cli/commands/unstar.ts index 67d39b23..a01cef1f 100644 --- a/dt-skill/src/cli/commands/unstar.ts +++ b/dt-skill/src/cli/commands/unstar.ts @@ -1,39 +1,39 @@ -import { apiRequest } from "../../http.js"; -import { ApiRoutes, ApiV1UnstarResponseSchema } from "../../schema/index.js"; -import { getRegistry } from "../registry.js"; -import type { GlobalOpts } from "../types.js"; -import { createSpinner, fail, formatError, isInteractive, promptConfirm } from "../ui.js"; +import { apiRequest } from '../../http.js'; +import { ApiRoutes, ApiV1UnstarResponseSchema } from '../../schema/index.js'; +import { getRegistry } from '../registry.js'; +import type { GlobalOpts } from '../types.js'; +import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'; export async function cmdUnstarSkill( - opts: GlobalOpts, - slugArg: string, - options: { yes?: boolean }, - inputAllowed: boolean, + opts: GlobalOpts, + slugArg: string, + options: { yes?: boolean }, + inputAllowed: boolean ) { - const slug = slugArg.trim().toLowerCase(); - if (!slug) fail("Slug required"); - const allowPrompt = isInteractive() && inputAllowed !== false; + const slug = slugArg.trim().toLowerCase(); + if (!slug) fail('Slug required'); + const allowPrompt = isInteractive() && inputAllowed !== false; - if (!options.yes) { - if (!allowPrompt) fail("Pass --yes (no input)"); - const ok = await promptConfirm(`Unstar ${slug}?`); - if (!ok) return undefined; - } + if (!options.yes) { + if (!allowPrompt) fail('Pass --yes (no input)'); + const ok = await promptConfirm(`Unstar ${slug}?`); + if (!ok) return undefined; + } - const registry = await getRegistry(opts, { cache: true }); - const spinner = createSpinner(`Unstarring ${slug}`); - try { - const result = await apiRequest( - registry, - { method: "DELETE", path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}` }, - ApiV1UnstarResponseSchema, - ); - spinner.succeed( - result.alreadyUnstarred ? `OK. ${slug} already unstarred.` : `OK. Unstarred ${slug}`, - ); - return result; - } catch (error) { - spinner.fail(formatError(error)); - throw error; - } + const registry = await getRegistry(opts, { cache: true }); + const spinner = createSpinner(`Unstarring ${slug}`); + try { + const result = await apiRequest( + registry, + { method: 'DELETE', path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}` }, + ApiV1UnstarResponseSchema + ); + spinner.succeed( + result.alreadyUnstarred ? `OK. ${slug} already unstarred.` : `OK. Unstarred ${slug}` + ); + return result; + } catch (error) { + spinner.fail(formatError(error)); + throw error; + } } diff --git a/dt-skill/src/cli/helpStyle.ts b/dt-skill/src/cli/helpStyle.ts index 02fc10ba..82f9eb1e 100644 --- a/dt-skill/src/cli/helpStyle.ts +++ b/dt-skill/src/cli/helpStyle.ts @@ -1,45 +1,45 @@ type Color = (value: string) => string; -function wrap(start: string, end = "\x1b[0m"): Color { - return (value) => `${start}${value}${end}`; +function wrap(start: string, end = '\x1b[0m'): Color { + return (value) => `${start}${value}${end}`; } const ansi = { - reset: "\x1b[0m", - bold: wrap("\x1b[1m"), - dim: wrap("\x1b[2m"), - cyan: wrap("\x1b[36m"), - green: wrap("\x1b[32m"), - yellow: wrap("\x1b[33m"), + reset: '\x1b[0m', + bold: wrap('\x1b[1m'), + dim: wrap('\x1b[2m'), + cyan: wrap('\x1b[36m'), + green: wrap('\x1b[32m'), + yellow: wrap('\x1b[33m'), }; function isColorEnabled() { - if (!process.stdout.isTTY) return false; - if (process.env.NO_COLOR) return false; - return true; + if (!process.stdout.isTTY) return false; + if (process.env.NO_COLOR) return false; + return true; } export function styleTitle(value: string) { - if (!isColorEnabled()) return value; - return `${ansi.bold(ansi.cyan(value))}${ansi.reset}`; + if (!isColorEnabled()) return value; + return `${ansi.bold(ansi.cyan(value))}${ansi.reset}`; } export function configureCommanderHelp(program: { - configureHelp: (config: { - sectionTitle?: (title: string) => string; - optionTerm?: (option: { flags: string }) => string; - commandTerm?: (cmd: { name: () => string }) => string; - }) => unknown; + configureHelp: (config: { + sectionTitle?: (title: string) => string; + optionTerm?: (option: { flags: string }) => string; + commandTerm?: (cmd: { name: () => string }) => string; + }) => unknown; }) { - if (!isColorEnabled()) return; - program.configureHelp({ - sectionTitle: (title) => ansi.bold(ansi.cyan(title)), - optionTerm: (option) => ansi.yellow(option.flags), - commandTerm: (cmd) => ansi.green(cmd.name()), - }); + if (!isColorEnabled()) return; + program.configureHelp({ + sectionTitle: (title) => ansi.bold(ansi.cyan(title)), + optionTerm: (option) => ansi.yellow(option.flags), + commandTerm: (cmd) => ansi.green(cmd.name()), + }); } export function styleEnvBlock(value: string) { - if (!isColorEnabled()) return value; - return `${ansi.dim(value)}${ansi.reset}`; + if (!isColorEnabled()) return value; + return `${ansi.dim(value)}${ansi.reset}`; } diff --git a/dt-skill/src/cli/prompts/search-multiselect.ts b/dt-skill/src/cli/prompts/search-multiselect.ts index 3d47d17a..5d2c5ec1 100644 --- a/dt-skill/src/cli/prompts/search-multiselect.ts +++ b/dt-skill/src/cli/prompts/search-multiselect.ts @@ -5,31 +5,31 @@ import pc from 'picocolors'; // Silent writable stream to prevent readline from echoing input const silentOutput = new Writable({ - write(_chunk, _encoding, callback) { - callback(); - }, + write(_chunk, _encoding, callback) { + callback(); + }, }); export interface SearchItem { - value: T; - label: string; - hint?: string; + value: T; + label: string; + hint?: string; } export interface LockedSection { - title: string; - items: SearchItem[]; + title: string; + items: SearchItem[]; } export interface SearchMultiselectOptions { - message: string; - items: SearchItem[]; - maxVisible?: number; - initialSelected?: T[]; - /** If true, require at least one item to be selected before submitting */ - required?: boolean; - /** Locked section shown above the searchable list - items are always selected and can't be toggled */ - lockedSection?: LockedSection; + message: string; + items: SearchItem[]; + maxVisible?: number; + initialSelected?: T[]; + /** If true, require at least one item to be selected before submitting */ + required?: boolean; + /** Locked section shown above the searchable list - items are always selected and can't be toggled */ + lockedSection?: LockedSection; } const S_STEP_ACTIVE = pc.green('◆'); @@ -49,81 +49,81 @@ export const cancelSymbol = Symbol('cancel'); * Matches common East Asian / emoji double-width behavior used by modern terminals. */ export function approxStringWidth(plain: string): number { - let width = 0; - for (const ch of plain) { - const code = ch.codePointAt(0)!; - if (code === 0) continue; - const wide = - (code >= 0x1100 && code <= 0x115f) || - (code >= 0x231a && code <= 0x231b) || - (code >= 0x2329 && code <= 0x232a) || - (code >= 0x23e9 && code <= 0x23ec) || - code === 0x23f0 || - code === 0x23f3 || - (code >= 0x25fd && code <= 0x25fe) || - (code >= 0x2614 && code <= 0x2615) || - (code >= 0x2648 && code <= 0x2653) || - (code >= 0x267f && code <= 0x267f) || - (code >= 0x2693 && code <= 0x2693) || - (code >= 0x26a1 && code <= 0x26a1) || - (code >= 0x26aa && code <= 0x26ab) || - (code >= 0x26bd && code <= 0x26be) || - (code >= 0x26c4 && code <= 0x26c5) || - (code >= 0x26ce && code <= 0x26ce) || - (code >= 0x26d4 && code <= 0x26d4) || - (code >= 0x26ea && code <= 0x26ea) || - (code >= 0x26f2 && code <= 0x26f3) || - (code >= 0x26f5 && code <= 0x26f5) || - (code >= 0x26fa && code <= 0x26fa) || - (code >= 0x26fd && code <= 0x26fd) || - (code >= 0x2705 && code <= 0x2705) || - (code >= 0x270a && code <= 0x270b) || - (code >= 0x2728 && code <= 0x2728) || - (code >= 0x274c && code <= 0x274c) || - (code >= 0x274e && code <= 0x274e) || - (code >= 0x2753 && code <= 0x2755) || - (code >= 0x2757 && code <= 0x2757) || - (code >= 0x2795 && code <= 0x2797) || - (code >= 0x27b0 && code <= 0x27b0) || - (code >= 0x27bf && code <= 0x27bf) || - (code >= 0x2b1b && code <= 0x2b1c) || - (code >= 0x2b50 && code <= 0x2b50) || - (code >= 0x2b55 && code <= 0x2b55) || - (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || - (code >= 0xa960 && code <= 0xa97c) || - (code >= 0xac00 && code <= 0xd7a3) || - (code >= 0xf900 && code <= 0xfaff) || - (code >= 0xfe10 && code <= 0xfe19) || - (code >= 0xfe30 && code <= 0xfe6f) || - (code >= 0xff00 && code <= 0xff60) || - (code >= 0xffe0 && code <= 0xffe6) || - (code >= 0x1f000 && code <= 0x1f9ff); - width += wide ? 2 : 1; - } - return width; + let width = 0; + for (const ch of plain) { + const code = ch.codePointAt(0)!; + if (code === 0) continue; + const wide = + (code >= 0x1100 && code <= 0x115f) || + (code >= 0x231a && code <= 0x231b) || + (code >= 0x2329 && code <= 0x232a) || + (code >= 0x23e9 && code <= 0x23ec) || + code === 0x23f0 || + code === 0x23f3 || + (code >= 0x25fd && code <= 0x25fe) || + (code >= 0x2614 && code <= 0x2615) || + (code >= 0x2648 && code <= 0x2653) || + (code >= 0x267f && code <= 0x267f) || + (code >= 0x2693 && code <= 0x2693) || + (code >= 0x26a1 && code <= 0x26a1) || + (code >= 0x26aa && code <= 0x26ab) || + (code >= 0x26bd && code <= 0x26be) || + (code >= 0x26c4 && code <= 0x26c5) || + (code >= 0x26ce && code <= 0x26ce) || + (code >= 0x26d4 && code <= 0x26d4) || + (code >= 0x26ea && code <= 0x26ea) || + (code >= 0x26f2 && code <= 0x26f3) || + (code >= 0x26f5 && code <= 0x26f5) || + (code >= 0x26fa && code <= 0x26fa) || + (code >= 0x26fd && code <= 0x26fd) || + (code >= 0x2705 && code <= 0x2705) || + (code >= 0x270a && code <= 0x270b) || + (code >= 0x2728 && code <= 0x2728) || + (code >= 0x274c && code <= 0x274c) || + (code >= 0x274e && code <= 0x274e) || + (code >= 0x2753 && code <= 0x2755) || + (code >= 0x2757 && code <= 0x2757) || + (code >= 0x2795 && code <= 0x2797) || + (code >= 0x27b0 && code <= 0x27b0) || + (code >= 0x27bf && code <= 0x27bf) || + (code >= 0x2b1b && code <= 0x2b1c) || + (code >= 0x2b50 && code <= 0x2b50) || + (code >= 0x2b55 && code <= 0x2b55) || + (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) || + (code >= 0xa960 && code <= 0xa97c) || + (code >= 0xac00 && code <= 0xd7a3) || + (code >= 0xf900 && code <= 0xfaff) || + (code >= 0xfe10 && code <= 0xfe19) || + (code >= 0xfe30 && code <= 0xfe6f) || + (code >= 0xff00 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + (code >= 0x1f000 && code <= 0x1f9ff); + width += wide ? 2 : 1; + } + return width; } /** * How many physical terminal rows one logical line occupies after soft-wrapping. */ export function visualRowsForLine(line: string, columns: number): number { - const plain = stripVTControlCharacters(line); - const cols = Math.max(1, columns); - const w = approxStringWidth(plain); - return Math.max(1, Math.ceil(w / cols)); + const plain = stripVTControlCharacters(line); + const cols = Math.max(1, columns); + const w = approxStringWidth(plain); + return Math.max(1, Math.ceil(w / cols)); } /** * Total physical rows for a block of logical lines (used to erase/redraw TUI output). */ export function countVisualRowsForLines(lines: string[], columns: number | undefined): number { - const cols = - columns !== undefined && columns > 0 - ? columns - : process.stdout.columns && process.stdout.columns > 0 - ? process.stdout.columns - : 80; - return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0); + const cols = + columns !== undefined && columns > 0 + ? columns + : process.stdout.columns && process.stdout.columns > 0 + ? process.stdout.columns + : 80; + return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0); } /** @@ -132,252 +132,264 @@ export function countVisualRowsForLines(lines: string[], columns: number | undef * Optionally supports a "locked" section that displays always-selected items. */ export async function searchMultiselect( - options: SearchMultiselectOptions + options: SearchMultiselectOptions ): Promise { - const { - message, - items, - maxVisible = 8, - initialSelected = [], - required = false, - lockedSection, - } = options; - - return new Promise((resolve) => { - const rl = readline.createInterface({ - input: process.stdin, - output: silentOutput, - terminal: false, - }); - - // Enable raw mode for keypress detection - if (process.stdin.isTTY) { - process.stdin.setRawMode(true); - } - readline.emitKeypressEvents(process.stdin, rl); - - let query = ''; - let cursor = 0; - const selected = new Set(initialSelected); - let lastRenderHeight = 0; - - // Locked items are always included in the result - const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : []; - - const filter = (item: SearchItem, q: string): boolean => { - if (!q) return true; - const lowerQ = q.toLowerCase(); - return ( - item.label.toLowerCase().includes(lowerQ) || - String(item.value).toLowerCase().includes(lowerQ) - ); - }; - - const getFiltered = (): SearchItem[] => { - return items.filter((item) => filter(item, query)); - }; - - const clearRender = (): void => { - if (lastRenderHeight > 0) { - // Move up and clear each line - process.stdout.write(`\x1b[${lastRenderHeight}A`); - for (let i = 0; i < lastRenderHeight; i++) { - process.stdout.write('\x1b[2K\x1b[1B'); - } - process.stdout.write(`\x1b[${lastRenderHeight}A`); - } - }; - - const render = (state: 'active' | 'submit' | 'cancel' = 'active'): void => { - clearRender(); - - const lines: string[] = []; - const filtered = getFiltered(); - - // Header - const icon = - state === 'active' ? S_STEP_ACTIVE : state === 'cancel' ? S_STEP_CANCEL : S_STEP_SUBMIT; - lines.push(`${icon} ${pc.bold(message)}`); - - if (state === 'active') { - // Locked section (universal agents) - if (lockedSection && lockedSection.items.length > 0) { - lines.push(`${S_BAR}`); - const lockedTitle = `${pc.bold(lockedSection.title)} ${pc.dim('── always included')}`; - lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`); - for (const item of lockedSection.items) { - lines.push(`${S_BAR} ${S_BULLET} ${pc.bold(item.label)}`); - } - lines.push(`${S_BAR}`); - lines.push( - `${S_BAR} ${S_BAR_H}${S_BAR_H} ${pc.bold('Additional agents')} ${S_BAR_H.repeat(29)}` - ); + const { + message, + items, + maxVisible = 8, + initialSelected = [], + required = false, + lockedSection, + } = options; + + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: silentOutput, + terminal: false, + }); + + // Enable raw mode for keypress detection + if (process.stdin.isTTY) { + process.stdin.setRawMode(true); } - - // Search input - const searchLine = `${S_BAR} ${pc.dim('Search:')} ${query}${pc.inverse(' ')}`; - lines.push(searchLine); - - // Hint - lines.push(`${S_BAR} ${pc.dim('↑↓ move, space select, enter confirm')}`); - lines.push(`${S_BAR}`); - - // Items - const visibleStart = Math.max( - 0, - Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible) - ); - const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible); - const visibleItems = filtered.slice(visibleStart, visibleEnd); - - if (filtered.length === 0) { - lines.push(`${S_BAR} ${pc.dim('No matches found')}`); - } else { - for (let i = 0; i < visibleItems.length; i++) { - const item = visibleItems[i]!; - const actualIndex = visibleStart + i; - const isSelected = selected.has(item.value); - const isCursor = actualIndex === cursor; - - const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE; - const label = isCursor ? pc.underline(item.label) : item.label; - const hint = item.hint ? pc.dim(` (${item.hint})`) : ''; - - const prefix = isCursor ? pc.cyan('❯') : ' '; - lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`); - } - - // Show count if more items - const hiddenBefore = visibleStart; - const hiddenAfter = filtered.length - visibleEnd; - if (hiddenBefore > 0 || hiddenAfter > 0) { - const parts: string[] = []; - if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`); - if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`); - lines.push(`${S_BAR} ${pc.dim(parts.join(' '))}`); - } - } - - // Selected summary (include locked items) - lines.push(`${S_BAR}`); - const allSelectedLabels = [ - ...(lockedSection ? lockedSection.items.map((i) => i.label) : []), - ...items.filter((item) => selected.has(item.value)).map((item) => item.label), - ]; - if (allSelectedLabels.length === 0) { - lines.push(`${S_BAR} ${pc.dim('Selected: (none)')}`); - } else { - const summary = - allSelectedLabels.length <= 3 - ? allSelectedLabels.join(', ') - : `${allSelectedLabels.slice(0, 3).join(', ')} +${allSelectedLabels.length - 3} more`; - lines.push(`${S_BAR} ${pc.green('Selected:')} ${summary}`); - } - - lines.push(`${pc.dim('└')}`); - } else if (state === 'submit') { - // Final state - show what was selected (including locked) - const allSelectedLabels = [ - ...(lockedSection ? lockedSection.items.map((i) => i.label) : []), - ...items.filter((item) => selected.has(item.value)).map((item) => item.label), - ]; - lines.push(`${S_BAR} ${pc.dim(allSelectedLabels.join(', '))}`); - } else if (state === 'cancel') { - lines.push(`${S_BAR} ${pc.strikethrough(pc.dim('Cancelled'))}`); - } - - process.stdout.write(lines.join('\n') + '\n'); - // Use wrapped row count: logical lines can span multiple terminal rows when hints - // or labels exceed column width. Using lines.length alone under-counts and breaks - // clearRender(), causing the prompt to re-print hundreds of times on each redraw. - lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns); - }; - - const cleanup = (): void => { - process.stdin.removeListener('keypress', keypressHandler); - if (process.stdin.isTTY) { - process.stdin.setRawMode(false); - } - rl.close(); - }; - - const submit = (): void => { - // If required and no locked items, don't allow submitting with no selection - if (required && selected.size === 0 && lockedValues.length === 0) { - return; - } - render('submit'); - cleanup(); - // Include locked values in the result - resolve([...lockedValues, ...Array.from(selected)]); - }; - - const cancel = (): void => { - render('cancel'); - cleanup(); - resolve(cancelSymbol); - }; - - // Handle keypresses - const keypressHandler = (_str: string, key: readline.Key): void => { - if (!key) return; - - const filtered = getFiltered(); - - if (key.name === 'return') { - submit(); - return; - } - - if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { - cancel(); - return; - } - - if (key.name === 'up') { - cursor = Math.max(0, cursor - 1); + readline.emitKeypressEvents(process.stdin, rl); + + let query = ''; + let cursor = 0; + const selected = new Set(initialSelected); + let lastRenderHeight = 0; + + // Locked items are always included in the result + const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : []; + + const filter = (item: SearchItem, q: string): boolean => { + if (!q) return true; + const lowerQ = q.toLowerCase(); + return ( + item.label.toLowerCase().includes(lowerQ) || + String(item.value).toLowerCase().includes(lowerQ) + ); + }; + + const getFiltered = (): SearchItem[] => { + return items.filter((item) => filter(item, query)); + }; + + const clearRender = (): void => { + if (lastRenderHeight > 0) { + // Move up and clear each line + process.stdout.write(`\x1b[${lastRenderHeight}A`); + for (let i = 0; i < lastRenderHeight; i++) { + process.stdout.write('\x1b[2K\x1b[1B'); + } + process.stdout.write(`\x1b[${lastRenderHeight}A`); + } + }; + + const render = (state: 'active' | 'submit' | 'cancel' = 'active'): void => { + clearRender(); + + const lines: string[] = []; + const filtered = getFiltered(); + + // Header + const icon = + state === 'active' + ? S_STEP_ACTIVE + : state === 'cancel' + ? S_STEP_CANCEL + : S_STEP_SUBMIT; + lines.push(`${icon} ${pc.bold(message)}`); + + if (state === 'active') { + // Locked section (universal agents) + if (lockedSection && lockedSection.items.length > 0) { + lines.push(`${S_BAR}`); + const lockedTitle = `${pc.bold(lockedSection.title)} ${pc.dim( + '── always included' + )}`; + lines.push( + `${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}` + ); + for (const item of lockedSection.items) { + lines.push(`${S_BAR} ${S_BULLET} ${pc.bold(item.label)}`); + } + lines.push(`${S_BAR}`); + lines.push( + `${S_BAR} ${S_BAR_H}${S_BAR_H} ${pc.bold( + 'Additional agents' + )} ${S_BAR_H.repeat(29)}` + ); + } + + // Search input + const searchLine = `${S_BAR} ${pc.dim('Search:')} ${query}${pc.inverse(' ')}`; + lines.push(searchLine); + + // Hint + lines.push(`${S_BAR} ${pc.dim('↑↓ move, space select, enter confirm')}`); + lines.push(`${S_BAR}`); + + // Items + const visibleStart = Math.max( + 0, + Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible) + ); + const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible); + const visibleItems = filtered.slice(visibleStart, visibleEnd); + + if (filtered.length === 0) { + lines.push(`${S_BAR} ${pc.dim('No matches found')}`); + } else { + for (let i = 0; i < visibleItems.length; i++) { + const item = visibleItems[i]!; + const actualIndex = visibleStart + i; + const isSelected = selected.has(item.value); + const isCursor = actualIndex === cursor; + + const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE; + const label = isCursor ? pc.underline(item.label) : item.label; + const hint = item.hint ? pc.dim(` (${item.hint})`) : ''; + + const prefix = isCursor ? pc.cyan('❯') : ' '; + lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`); + } + + // Show count if more items + const hiddenBefore = visibleStart; + const hiddenAfter = filtered.length - visibleEnd; + if (hiddenBefore > 0 || hiddenAfter > 0) { + const parts: string[] = []; + if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`); + if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`); + lines.push(`${S_BAR} ${pc.dim(parts.join(' '))}`); + } + } + + // Selected summary (include locked items) + lines.push(`${S_BAR}`); + const allSelectedLabels = [ + ...(lockedSection ? lockedSection.items.map((i) => i.label) : []), + ...items.filter((item) => selected.has(item.value)).map((item) => item.label), + ]; + if (allSelectedLabels.length === 0) { + lines.push(`${S_BAR} ${pc.dim('Selected: (none)')}`); + } else { + const summary = + allSelectedLabels.length <= 3 + ? allSelectedLabels.join(', ') + : `${allSelectedLabels.slice(0, 3).join(', ')} +${ + allSelectedLabels.length - 3 + } more`; + lines.push(`${S_BAR} ${pc.green('Selected:')} ${summary}`); + } + + lines.push(`${pc.dim('└')}`); + } else if (state === 'submit') { + // Final state - show what was selected (including locked) + const allSelectedLabels = [ + ...(lockedSection ? lockedSection.items.map((i) => i.label) : []), + ...items.filter((item) => selected.has(item.value)).map((item) => item.label), + ]; + lines.push(`${S_BAR} ${pc.dim(allSelectedLabels.join(', '))}`); + } else if (state === 'cancel') { + lines.push(`${S_BAR} ${pc.strikethrough(pc.dim('Cancelled'))}`); + } + + process.stdout.write(lines.join('\n') + '\n'); + // Use wrapped row count: logical lines can span multiple terminal rows when hints + // or labels exceed column width. Using lines.length alone under-counts and breaks + // clearRender(), causing the prompt to re-print hundreds of times on each redraw. + lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns); + }; + + const cleanup = (): void => { + process.stdin.removeListener('keypress', keypressHandler); + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + rl.close(); + }; + + const submit = (): void => { + // If required and no locked items, don't allow submitting with no selection + if (required && selected.size === 0 && lockedValues.length === 0) { + return; + } + render('submit'); + cleanup(); + // Include locked values in the result + resolve([...lockedValues, ...Array.from(selected)]); + }; + + const cancel = (): void => { + render('cancel'); + cleanup(); + resolve(cancelSymbol); + }; + + // Handle keypresses + const keypressHandler = (_str: string, key: readline.Key): void => { + if (!key) return; + + const filtered = getFiltered(); + + if (key.name === 'return') { + submit(); + return; + } + + if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { + cancel(); + return; + } + + if (key.name === 'up') { + cursor = Math.max(0, cursor - 1); + render(); + return; + } + + if (key.name === 'down') { + cursor = Math.min(filtered.length - 1, cursor + 1); + render(); + return; + } + + if (key.name === 'space') { + const item = filtered[cursor]; + if (item) { + if (selected.has(item.value)) { + selected.delete(item.value); + } else { + selected.add(item.value); + } + } + render(); + return; + } + + if (key.name === 'backspace') { + query = query.slice(0, -1); + cursor = 0; + render(); + return; + } + + // Regular character input + if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { + query += key.sequence; + cursor = 0; + render(); + return; + } + }; + + process.stdin.on('keypress', keypressHandler); + + // Initial render render(); - return; - } - - if (key.name === 'down') { - cursor = Math.min(filtered.length - 1, cursor + 1); - render(); - return; - } - - if (key.name === 'space') { - const item = filtered[cursor]; - if (item) { - if (selected.has(item.value)) { - selected.delete(item.value); - } else { - selected.add(item.value); - } - } - render(); - return; - } - - if (key.name === 'backspace') { - query = query.slice(0, -1); - cursor = 0; - render(); - return; - } - - // Regular character input - if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { - query += key.sequence; - cursor = 0; - render(); - return; - } - }; - - process.stdin.on('keypress', keypressHandler); - - // Initial render - render(); - }); + }); } diff --git a/dt-skill/src/cli/registry.test.ts b/dt-skill/src/cli/registry.test.ts index a0562feb..0fe0dde3 100644 --- a/dt-skill/src/cli/registry.test.ts +++ b/dt-skill/src/cli/registry.test.ts @@ -1,101 +1,103 @@ /* @vitest-environment node */ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { GlobalOpts } from "./types"; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { GlobalOpts } from './types'; const readGlobalConfig = vi.fn(); const writeGlobalConfig = vi.fn(); const discoverRegistryFromSite = vi.fn(); -vi.mock("../config.js", () => ({ - readGlobalConfig: (...args: unknown[]) => readGlobalConfig(...args), - writeGlobalConfig: (...args: unknown[]) => writeGlobalConfig(...args), +vi.mock('../config.js', () => ({ + readGlobalConfig: (...args: unknown[]) => readGlobalConfig(...args), + writeGlobalConfig: (...args: unknown[]) => writeGlobalConfig(...args), })); -vi.mock("../discovery.js", () => ({ - discoverRegistryFromSite: (...args: unknown[]) => discoverRegistryFromSite(...args), +vi.mock('../discovery.js', () => ({ + discoverRegistryFromSite: (...args: unknown[]) => discoverRegistryFromSite(...args), })); -const { DEFAULT_REGISTRY, DEFAULT_SITE, getRegistry, resolveRegistry } = await import("./registry"); +const { DEFAULT_REGISTRY, DEFAULT_SITE, getRegistry, resolveRegistry } = await import('./registry'); function makeOpts(overrides: Partial = {}): GlobalOpts { - return { - workdir: "/work", - dir: "/work/skills", - site: "", - registry: DEFAULT_REGISTRY, - registrySource: "default", - ...overrides, - }; + return { + workdir: '/work', + dir: '/work/skills', + site: '', + registry: DEFAULT_REGISTRY, + registrySource: 'default', + ...overrides, + }; } beforeEach(() => { - readGlobalConfig.mockReset(); - writeGlobalConfig.mockReset(); - discoverRegistryFromSite.mockReset(); + readGlobalConfig.mockReset(); + writeGlobalConfig.mockReset(); + discoverRegistryFromSite.mockReset(); }); -describe("registry resolution", () => { - it("has no static site or registry fallback", () => { - expect(DEFAULT_SITE).toBe(""); - expect(DEFAULT_REGISTRY).toBe(""); - }); +describe('registry resolution', () => { + it('has no static site or registry fallback', () => { + expect(DEFAULT_SITE).toBe(''); + expect(DEFAULT_REGISTRY).toBe(''); + }); - it("prefers explicit registry over discovery/cache", async () => { - readGlobalConfig.mockResolvedValue({ registry: "https://cached.example" }); - discoverRegistryFromSite.mockResolvedValue({ apiBase: "https://discovered.example" }); + it('prefers explicit registry over discovery/cache', async () => { + readGlobalConfig.mockResolvedValue({ registry: 'https://cached.example' }); + discoverRegistryFromSite.mockResolvedValue({ apiBase: 'https://discovered.example' }); - const registry = await resolveRegistry( - makeOpts({ registry: "https://custom.example", registrySource: "cli" }), - ); + const registry = await resolveRegistry( + makeOpts({ registry: 'https://custom.example', registrySource: 'cli' }) + ); - expect(registry).toBe("https://custom.example"); - expect(discoverRegistryFromSite).not.toHaveBeenCalled(); - }); + expect(registry).toBe('https://custom.example'); + expect(discoverRegistryFromSite).not.toHaveBeenCalled(); + }); - it("uses cached registry before site discovery", async () => { - readGlobalConfig.mockResolvedValue({ registry: "http://10.0.0.7:7001" }); - discoverRegistryFromSite.mockResolvedValue({ apiBase: "http://10.0.0.8:7001" }); + it('uses cached registry before site discovery', async () => { + readGlobalConfig.mockResolvedValue({ registry: 'http://10.0.0.7:7001' }); + discoverRegistryFromSite.mockResolvedValue({ apiBase: 'http://10.0.0.8:7001' }); - const registry = await resolveRegistry(makeOpts({ site: "http://10.0.0.8:7001" })); + const registry = await resolveRegistry(makeOpts({ site: 'http://10.0.0.8:7001' })); - expect(registry).toBe("http://10.0.0.7:7001"); - expect(discoverRegistryFromSite).not.toHaveBeenCalled(); - }); + expect(registry).toBe('http://10.0.0.7:7001'); + expect(discoverRegistryFromSite).not.toHaveBeenCalled(); + }); - it("discovers registry from site when cache is empty", async () => { - readGlobalConfig.mockResolvedValue(null); - discoverRegistryFromSite.mockResolvedValue({ apiBase: "http://10.0.0.8:7001" }); + it('discovers registry from site when cache is empty', async () => { + readGlobalConfig.mockResolvedValue(null); + discoverRegistryFromSite.mockResolvedValue({ apiBase: 'http://10.0.0.8:7001' }); - const registry = await getRegistry(makeOpts({ site: "http://10.0.0.8:7001" }), { cache: true }); + const registry = await getRegistry(makeOpts({ site: 'http://10.0.0.8:7001' }), { + cache: true, + }); - expect(registry).toBe("http://10.0.0.8:7001"); - expect(writeGlobalConfig).toHaveBeenCalledWith({ - registry: "http://10.0.0.8:7001", + expect(registry).toBe('http://10.0.0.8:7001'); + expect(writeGlobalConfig).toHaveBeenCalledWith({ + registry: 'http://10.0.0.8:7001', + }); }); - }); - it("fails clearly when no explicit, cached, or discoverable registry exists", async () => { - readGlobalConfig.mockResolvedValue(null); - discoverRegistryFromSite.mockResolvedValue(null); + it('fails clearly when no explicit, cached, or discoverable registry exists', async () => { + readGlobalConfig.mockResolvedValue(null); + discoverRegistryFromSite.mockResolvedValue(null); - await expect(getRegistry(makeOpts(), { cache: true })).rejects.toThrow( - "Registry is not configured", - ); - expect(writeGlobalConfig).not.toHaveBeenCalled(); - }); + await expect(getRegistry(makeOpts(), { cache: true })).rejects.toThrow( + 'Registry is not configured' + ); + expect(writeGlobalConfig).not.toHaveBeenCalled(); + }); - it("caches an explicit runtime registry even when another custom registry was cached", async () => { - readGlobalConfig.mockResolvedValue({ registry: "http://10.0.0.7:7001" }); + it('caches an explicit runtime registry even when another custom registry was cached', async () => { + readGlobalConfig.mockResolvedValue({ registry: 'http://10.0.0.7:7001' }); - const registry = await getRegistry( - makeOpts({ registry: "http://10.0.0.8:7001", registrySource: "cli" }), - { cache: true }, - ); + const registry = await getRegistry( + makeOpts({ registry: 'http://10.0.0.8:7001', registrySource: 'cli' }), + { cache: true } + ); - expect(registry).toBe("http://10.0.0.8:7001"); - expect(writeGlobalConfig).toHaveBeenCalledWith({ - registry: "http://10.0.0.8:7001", + expect(registry).toBe('http://10.0.0.8:7001'); + expect(writeGlobalConfig).toHaveBeenCalledWith({ + registry: 'http://10.0.0.8:7001', + }); }); - }); }); diff --git a/dt-skill/src/cli/registry.ts b/dt-skill/src/cli/registry.ts index 9391888a..97681a58 100644 --- a/dt-skill/src/cli/registry.ts +++ b/dt-skill/src/cli/registry.ts @@ -1,36 +1,36 @@ -import { readGlobalConfig, writeGlobalConfig } from "../config.js"; -import { discoverRegistryFromSite } from "../discovery.js"; -import type { GlobalOpts } from "./types.js"; +import { readGlobalConfig, writeGlobalConfig } from '../config.js'; +import { discoverRegistryFromSite } from '../discovery.js'; +import type { GlobalOpts } from './types.js'; -export const DEFAULT_SITE = ""; -export const DEFAULT_REGISTRY = ""; +export const DEFAULT_SITE = ''; +export const DEFAULT_REGISTRY = ''; export async function resolveRegistry(opts: GlobalOpts) { - const explicit = opts.registrySource !== "default" ? opts.registry.trim() : ""; - if (explicit) return explicit; + const explicit = opts.registrySource !== 'default' ? opts.registry.trim() : ''; + if (explicit) return explicit; - const cfg = await readGlobalConfig(); - const cached = cfg?.registry?.trim(); - if (cached) return cached; + const cfg = await readGlobalConfig(); + const cached = cfg?.registry?.trim(); + if (cached) return cached; - const site = opts.site.trim(); - if (site) { - const discovery = await discoverRegistryFromSite(site).catch(() => null); - const discovered = discovery?.apiBase?.trim(); - if (discovered) return discovered; - } + const site = opts.site.trim(); + if (site) { + const discovery = await discoverRegistryFromSite(site).catch(() => null); + const discovered = discovery?.apiBase?.trim(); + if (discovered) return discovered; + } - throw new Error( - "Registry is not configured. Copy a command from the Doraemon Skills page or pass --registry .", - ); + throw new Error( + 'Registry is not configured. Copy a command from the Doraemon Skills page or pass --registry .' + ); } export async function getRegistry(opts: GlobalOpts, params?: { cache?: boolean }) { - const cache = params?.cache !== false; - const registry = await resolveRegistry(opts); - if (!cache) return registry; - const cfg = await readGlobalConfig(); - const cached = cfg?.registry?.trim(); - if (!cached || cached !== registry) await writeGlobalConfig({ registry }); - return registry; + const cache = params?.cache !== false; + const registry = await resolveRegistry(opts); + if (!cache) return registry; + const cfg = await readGlobalConfig(); + const cached = cfg?.registry?.trim(); + if (!cached || cached !== registry) await writeGlobalConfig({ registry }); + return registry; } diff --git a/dt-skill/src/cli/scanSkills.test.ts b/dt-skill/src/cli/scanSkills.test.ts index 30e1b616..1bc9959d 100644 --- a/dt-skill/src/cli/scanSkills.test.ts +++ b/dt-skill/src/cli/scanSkills.test.ts @@ -1,57 +1,57 @@ /* @vitest-environment node */ -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; -import { describe, expect, it } from "vitest"; -import { findSkillFolders } from "./scanSkills"; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { findSkillFolders } from './scanSkills'; async function makeTmpDir() { - return mkdtemp(join(tmpdir(), "dt-skill-scan-")); + return mkdtemp(join(tmpdir(), 'dt-skill-scan-')); } -describe("scanSkills", () => { - it("detects a single skill folder (root contains SKILL.md)", async () => { - const root = await makeTmpDir(); - try { - await writeFile(join(root, "SKILL.md"), "# Skill\n", "utf8"); - const found = await findSkillFolders(root); - expect(found).toHaveLength(1); - expect(found[0]?.folder).toBe(resolve(root)); - expect(found[0]?.slug).toBeTruthy(); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); +describe('scanSkills', () => { + it('detects a single skill folder (root contains SKILL.md)', async () => { + const root = await makeTmpDir(); + try { + await writeFile(join(root, 'SKILL.md'), '# Skill\n', 'utf8'); + const found = await findSkillFolders(root); + expect(found).toHaveLength(1); + expect(found[0]?.folder).toBe(resolve(root)); + expect(found[0]?.slug).toBeTruthy(); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); - it("detects skills in a skills directory (subfolders)", async () => { - const root = await makeTmpDir(); - try { - const skillsDir = join(root, "skills"); - const folder = join(skillsDir, "cool-skill"); - await mkdir(folder, { recursive: true }); - await writeFile(join(folder, "SKILL.md"), "# Skill\n", "utf8"); + it('detects skills in a skills directory (subfolders)', async () => { + const root = await makeTmpDir(); + try { + const skillsDir = join(root, 'skills'); + const folder = join(skillsDir, 'cool-skill'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'SKILL.md'), '# Skill\n', 'utf8'); - const found = await findSkillFolders(skillsDir); - expect(found).toHaveLength(1); - expect(found[0]?.slug).toBe("cool-skill"); - expect(found[0]?.folder).toBe(resolve(folder)); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); + const found = await findSkillFolders(skillsDir); + expect(found).toHaveLength(1); + expect(found[0]?.slug).toBe('cool-skill'); + expect(found[0]?.folder).toBe(resolve(folder)); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); - it("ignores plural skills.md marker files", async () => { - const root = await makeTmpDir(); - try { - const folder = join(root, "docs"); - await mkdir(folder, { recursive: true }); - await writeFile(join(folder, "skills.md"), "# Docs\n", "utf8"); + it('ignores plural skills.md marker files', async () => { + const root = await makeTmpDir(); + try { + const folder = join(root, 'docs'); + await mkdir(folder, { recursive: true }); + await writeFile(join(folder, 'skills.md'), '# Docs\n', 'utf8'); - const found = await findSkillFolders(root); - expect(found).toHaveLength(0); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); + const found = await findSkillFolders(root); + expect(found).toHaveLength(0); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); }); diff --git a/dt-skill/src/cli/scanSkills.ts b/dt-skill/src/cli/scanSkills.ts index be022047..8ac4aca6 100644 --- a/dt-skill/src/cli/scanSkills.ts +++ b/dt-skill/src/cli/scanSkills.ts @@ -1,49 +1,49 @@ -import { readdir, stat } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; -import { sanitizeSlug, titleCase } from "./slug.js"; +import { readdir, stat } from 'node:fs/promises'; +import { basename, join, resolve } from 'node:path'; +import { sanitizeSlug, titleCase } from './slug.js'; export type SkillFolder = { - folder: string; - slug: string; - displayName: string; + folder: string; + slug: string; + displayName: string; }; export async function findSkillFolders(root: string): Promise { - const absRoot = resolve(root); - const rootStat = await stat(absRoot).catch(() => null); - if (!rootStat || !rootStat.isDirectory()) return []; + const absRoot = resolve(root); + const rootStat = await stat(absRoot).catch(() => null); + if (!rootStat || !rootStat.isDirectory()) return []; - const direct = await isSkillFolder(absRoot); - if (direct) return [direct]; + const direct = await isSkillFolder(absRoot); + if (direct) return [direct]; - const entries = await readdir(absRoot, { withFileTypes: true }).catch(() => []); - const folders = entries - .filter((entry) => entry.isDirectory()) - .map((entry) => join(absRoot, entry.name)); - const results: SkillFolder[] = []; - for (const folder of folders) { - const found = await isSkillFolder(folder); - if (found) results.push(found); - } - return results.sort((a, b) => a.slug.localeCompare(b.slug)); + const entries = await readdir(absRoot, { withFileTypes: true }).catch(() => []); + const folders = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => join(absRoot, entry.name)); + const results: SkillFolder[] = []; + for (const folder of folders) { + const found = await isSkillFolder(folder); + if (found) results.push(found); + } + return results.sort((a, b) => a.slug.localeCompare(b.slug)); } async function isSkillFolder(folder: string): Promise { - const marker = await findSkillMarker(folder); - if (!marker) return null; - const base = basename(folder); - const slug = sanitizeSlug(base); - if (!slug) return null; - const displayName = titleCase(base); - return { folder, slug, displayName }; + const marker = await findSkillMarker(folder); + if (!marker) return null; + const base = basename(folder); + const slug = sanitizeSlug(base); + if (!slug) return null; + const displayName = titleCase(base); + return { folder, slug, displayName }; } async function findSkillMarker(folder: string) { - const candidates = ["SKILL.md", "skill.md"]; - for (const name of candidates) { - const path = join(folder, name); - const st = await stat(path).catch(() => null); - if (st?.isFile()) return path; - } - return null; + const candidates = ['SKILL.md', 'skill.md']; + for (const name of candidates) { + const path = join(folder, name); + const st = await stat(path).catch(() => null); + if (st?.isFile()) return path; + } + return null; } diff --git a/dt-skill/src/cli/slug.ts b/dt-skill/src/cli/slug.ts index ae7d8f40..3a8e8122 100644 --- a/dt-skill/src/cli/slug.ts +++ b/dt-skill/src/cli/slug.ts @@ -1,16 +1,16 @@ export function sanitizeSlug(value: string) { - const raw = value - .trim() - .toLowerCase() - .replace(/[^a-z0-9-]+/g, "-"); - const cleaned = raw.replace(/^-+/, "").replace(/-+$/, "").replace(/--+/g, "-"); - return cleaned; + const raw = value + .trim() + .toLowerCase() + .replace(/[^a-z0-9-]+/g, '-'); + const cleaned = raw.replace(/^-+/, '').replace(/-+$/, '').replace(/--+/g, '-'); + return cleaned; } export function titleCase(value: string) { - return value - .trim() - .replace(/[-_]+/g, " ") - .replace(/\s+/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()); + return value + .trim() + .replace(/[-_]+/g, ' ') + .replace(/\s+/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()); } diff --git a/dt-skill/src/cli/types.ts b/dt-skill/src/cli/types.ts index c49a8f19..7f2556b5 100644 --- a/dt-skill/src/cli/types.ts +++ b/dt-skill/src/cli/types.ts @@ -1,15 +1,15 @@ export type GlobalOpts = { - workdir: string; - dir: string; - site: string; - registry: string; - registrySource: "cli" | "env" | "default"; - agent?: string; - globalScope?: boolean; - globalScopeExplicit?: boolean; + workdir: string; + dir: string; + site: string; + registry: string; + registrySource: 'cli' | 'env' | 'default'; + agent?: string; + globalScope?: boolean; + globalScopeExplicit?: boolean; }; export type ResolveResult = { - match: { version: string } | null; - latestVersion: { version: string } | null; + match: { version: string } | null; + latestVersion: { version: string } | null; }; diff --git a/dt-skill/src/cli/ui.test.ts b/dt-skill/src/cli/ui.test.ts index 12a23d15..90bc4282 100644 --- a/dt-skill/src/cli/ui.test.ts +++ b/dt-skill/src/cli/ui.test.ts @@ -1,77 +1,77 @@ /* @vitest-environment node */ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from 'vitest'; const mockSpawn = vi.fn(); const originalPlatform = process.platform; -vi.mock("node:child_process", () => ({ - spawn: (...args: unknown[]) => mockSpawn(...args), +vi.mock('node:child_process', () => ({ + spawn: (...args: unknown[]) => mockSpawn(...args), })); -const { openInBrowser } = await import("./ui"); +const { openInBrowser } = await import('./ui'); type ErrorHandler = (error: NodeJS.ErrnoException) => void; function createMockChild() { - let onError: ErrorHandler | null = null; - const child = { - on: vi.fn((event: string, handler: ErrorHandler) => { - if (event === "error") onError = handler; - return child; - }), - unref: vi.fn(), - emitError: (error: NodeJS.ErrnoException) => onError?.(error), - }; - return child; + let onError: ErrorHandler | null = null; + const child = { + on: vi.fn((event: string, handler: ErrorHandler) => { + if (event === 'error') onError = handler; + return child; + }), + unref: vi.fn(), + emitError: (error: NodeJS.ErrnoException) => onError?.(error), + }; + return child; } -describe("openInBrowser", () => { - it("uses explorer on Windows and preserves query params in the URL argument", () => { - const child = createMockChild(); - mockSpawn.mockReturnValueOnce(child); - const url = - "https://example.com/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2Fcallback&state=abc123"; +describe('openInBrowser', () => { + it('uses explorer on Windows and preserves query params in the URL argument', () => { + const child = createMockChild(); + mockSpawn.mockReturnValueOnce(child); + const url = + 'https://example.com/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2Fcallback&state=abc123'; - try { - Object.defineProperty(process, "platform", { value: "win32" }); - openInBrowser(url); - } finally { - Object.defineProperty(process, "platform", { value: originalPlatform }); - } + try { + Object.defineProperty(process, 'platform', { value: 'win32' }); + openInBrowser(url); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } - expect(mockSpawn).toHaveBeenCalledWith("explorer", [url], { - stdio: "ignore", - detached: true, + expect(mockSpawn).toHaveBeenCalledWith('explorer', [url], { + stdio: 'ignore', + detached: true, + }); + expect(child.unref).toHaveBeenCalledOnce(); }); - expect(child.unref).toHaveBeenCalledOnce(); - }); - it("prints manual URL instructions when browser opener is missing", () => { - const child = createMockChild(); - mockSpawn.mockReturnValueOnce(child); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + it('prints manual URL instructions when browser opener is missing', () => { + const child = createMockChild(); + mockSpawn.mockReturnValueOnce(child); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - openInBrowser("https://example.com"); - child.emitError(Object.assign(new Error("not found"), { code: "ENOENT" })); + openInBrowser('https://example.com'); + child.emitError(Object.assign(new Error('not found'), { code: 'ENOENT' })); - expect(logSpy).toHaveBeenCalledWith("Could not open browser automatically."); - expect(logSpy).toHaveBeenCalledWith("Please open this URL manually:"); - expect(logSpy).toHaveBeenCalledWith(" https://example.com"); - expect(child.unref).toHaveBeenCalledOnce(); - logSpy.mockRestore(); - }); + expect(logSpy).toHaveBeenCalledWith('Could not open browser automatically.'); + expect(logSpy).toHaveBeenCalledWith('Please open this URL manually:'); + expect(logSpy).toHaveBeenCalledWith(' https://example.com'); + expect(child.unref).toHaveBeenCalledOnce(); + logSpy.mockRestore(); + }); - it("does not print manual instructions for non-ENOENT errors", () => { - const child = createMockChild(); - mockSpawn.mockReturnValueOnce(child); - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + it('does not print manual instructions for non-ENOENT errors', () => { + const child = createMockChild(); + mockSpawn.mockReturnValueOnce(child); + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - openInBrowser("https://example.com"); - child.emitError(Object.assign(new Error("permission denied"), { code: "EACCES" })); + openInBrowser('https://example.com'); + child.emitError(Object.assign(new Error('permission denied'), { code: 'EACCES' })); - expect(logSpy).not.toHaveBeenCalledWith("Could not open browser automatically."); - expect(child.unref).toHaveBeenCalledOnce(); - logSpy.mockRestore(); - }); + expect(logSpy).not.toHaveBeenCalledWith('Could not open browser automatically.'); + expect(child.unref).toHaveBeenCalledOnce(); + logSpy.mockRestore(); + }); }); diff --git a/dt-skill/src/cli/ui.ts b/dt-skill/src/cli/ui.ts index 12998c7b..e679face 100644 --- a/dt-skill/src/cli/ui.ts +++ b/dt-skill/src/cli/ui.ts @@ -1,137 +1,141 @@ -import { spawn } from "node:child_process"; -import { stdin } from "node:process"; -import { confirm, isCancel, select } from "@clack/prompts"; -import ora from "ora"; -import { listAgentNames, getAgentLabel, resolveAgentWorkdir, AGENTS } from "./agents.js"; -import type { AgentName } from "./agents.js"; +import { spawn } from 'node:child_process'; +import { stdin } from 'node:process'; +import { confirm, isCancel, select } from '@clack/prompts'; +import ora from 'ora'; +import { listAgentNames, getAgentLabel, resolveAgentWorkdir, AGENTS } from './agents.js'; +import type { AgentName } from './agents.js'; export async function promptHidden(prompt: string) { - if (!stdin.isTTY) return ""; - process.stdout.write(prompt); - const chunks: Buffer[] = []; - stdin.setRawMode(true); - stdin.resume(); - return new Promise((resolvePromise) => { - function onData(data: Buffer) { - const text = data.toString("utf8"); - if (text === "\r" || text === "\n") { - stdin.setRawMode(false); - stdin.pause(); - stdin.off("data", onData); - process.stdout.write("\n"); - resolvePromise(Buffer.concat(chunks).toString("utf8").trim()); - return; - } - if (text === "\u0003") { - stdin.setRawMode(false); - stdin.pause(); - stdin.off("data", onData); - process.stdout.write("\n"); - fail("Canceled"); - } - if (text === "\u007f") { - chunks.pop(); - return; - } - chunks.push(data); - } - stdin.on("data", onData); - }); + if (!stdin.isTTY) return ''; + process.stdout.write(prompt); + const chunks: Buffer[] = []; + stdin.setRawMode(true); + stdin.resume(); + return new Promise((resolvePromise) => { + function onData(data: Buffer) { + const text = data.toString('utf8'); + if (text === '\r' || text === '\n') { + stdin.setRawMode(false); + stdin.pause(); + stdin.off('data', onData); + process.stdout.write('\n'); + resolvePromise(Buffer.concat(chunks).toString('utf8').trim()); + return; + } + if (text === '\u0003') { + stdin.setRawMode(false); + stdin.pause(); + stdin.off('data', onData); + process.stdout.write('\n'); + fail('Canceled'); + } + if (text === '\u007f') { + chunks.pop(); + return; + } + chunks.push(data); + } + stdin.on('data', onData); + }); } export async function promptConfirm(prompt: string) { - const answer = await confirm({ message: prompt }); - if (isCancel(answer)) return false; - return answer; + const answer = await confirm({ message: prompt }); + if (isCancel(answer)) return false; + return answer; } export function openInBrowser(url: string) { - const args = - process.platform === "darwin" - ? ["open", url] - : process.platform === "win32" - ? ["explorer", url] - : ["xdg-open", url]; - const [command, ...commandArgs] = args; - if (!command) return; - - const child = spawn(command, commandArgs, { stdio: "ignore", detached: true }); - - child.on("error", (err) => { - if ((err as NodeJS.ErrnoException).code === "ENOENT") { - console.log(""); - console.log("Could not open browser automatically."); - console.log("Please open this URL manually:"); - console.log(""); - console.log(` ${url}`); - console.log(""); - } - }); - - child.unref(); + const args = + process.platform === 'darwin' + ? ['open', url] + : process.platform === 'win32' + ? ['explorer', url] + : ['xdg-open', url]; + const [command, ...commandArgs] = args; + if (!command) return; + + const child = spawn(command, commandArgs, { stdio: 'ignore', detached: true }); + + child.on('error', (err) => { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + console.log(''); + console.log('Could not open browser automatically.'); + console.log('Please open this URL manually:'); + console.log(''); + console.log(` ${url}`); + console.log(''); + } + }); + + child.unref(); } export function isInteractive() { - return process.stdout.isTTY && stdin.isTTY; + return process.stdout.isTTY && stdin.isTTY; } export function createSpinner(text: string) { - return ora({ text, spinner: "dots", isEnabled: isInteractive() }).start(); + return ora({ text, spinner: 'dots', isEnabled: isInteractive() }).start(); } export function formatError(error: unknown) { - if (error instanceof Error) return error.message; - return String(error); + if (error instanceof Error) return error.message; + return String(error); } export function fail(message: string): never { - throw new Error(message); + throw new Error(message); } -export async function selectAgent(): Promise<{ agent: AgentName; workdir: string; dir: string } | null> { - if (!isInteractive()) return null; - - const names = listAgentNames(); - const options = names.map((name) => ({ - value: name, - label: getAgentLabel(name), - })); - - const selected = await select({ - message: "Select target agent:", - options, - }); - - if (isCancel(selected)) return null; - const agent = selected as AgentName; - const workdir = resolveAgentWorkdir(agent, false); - const dir = `${workdir}/skills`; - return { agent, workdir, dir }; +export async function selectAgent(): Promise<{ + agent: AgentName; + workdir: string; + dir: string; +} | null> { + if (!isInteractive()) return null; + + const names = listAgentNames(); + const options = names.map((name) => ({ + value: name, + label: getAgentLabel(name), + })); + + const selected = await select({ + message: 'Select target agent:', + options, + }); + + if (isCancel(selected)) return null; + const agent = selected as AgentName; + const workdir = resolveAgentWorkdir(agent, false); + const dir = `${workdir}/skills`; + return { agent, workdir, dir }; } export async function selectScope(agent: AgentName): Promise { - if (!isInteractive()) return null; - - // Check if the selected agent supports global installation - const supportsGlobal = AGENTS[agent].globalWorkdir !== undefined; - if (!supportsGlobal) return false; - - const scope = await select({ - message: "安装范围", - options: [ - { - value: false, - label: "Project", - hint: "在当前目录安装(随项目提交)", - }, - { - value: true, - label: "Global", - hint: "在 home 目录安装(跨项目可用)", - }, - ], - }); - - if (isCancel(scope)) return null; - return scope as boolean; + if (!isInteractive()) return null; + + // Check if the selected agent supports global installation + const supportsGlobal = AGENTS[agent].globalWorkdir !== undefined; + if (!supportsGlobal) return false; + + const scope = await select({ + message: '安装范围', + options: [ + { + value: false, + label: 'Project', + hint: '在当前目录安装(随项目提交)', + }, + { + value: true, + label: 'Global', + hint: '在 home 目录安装(跨项目可用)', + }, + ], + }); + + if (isCancel(scope)) return null; + return scope as boolean; } diff --git a/dt-skill/src/config.test.ts b/dt-skill/src/config.test.ts index a378538a..3c3e105e 100644 --- a/dt-skill/src/config.test.ts +++ b/dt-skill/src/config.test.ts @@ -1,87 +1,91 @@ /* @vitest-environment node */ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createEnvStubRegistry } from "../test/runtimeStubs.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createEnvStubRegistry } from '../test/runtimeStubs.js'; const fsMocks = vi.hoisted(() => ({ - chmod: vi.fn(), - mkdir: vi.fn(), - readFile: vi.fn(), - writeFile: vi.fn(), + chmod: vi.fn(), + mkdir: vi.fn(), + readFile: vi.fn(), + writeFile: vi.fn(), })); -vi.mock("node:fs/promises", async () => { - const actual = await vi.importActual("node:fs/promises"); - return { - ...actual, - chmod: fsMocks.chmod, - mkdir: fsMocks.mkdir, - readFile: fsMocks.readFile, - writeFile: fsMocks.writeFile, - }; +vi.mock('node:fs/promises', async () => { + const actual = await vi.importActual('node:fs/promises'); + return { + ...actual, + chmod: fsMocks.chmod, + mkdir: fsMocks.mkdir, + readFile: fsMocks.readFile, + writeFile: fsMocks.writeFile, + }; }); -const configModuleSpecifier = "./config.js?config-test" as string; +const configModuleSpecifier = './config.js?config-test' as string; -const { writeGlobalConfig } = (await import(configModuleSpecifier)) as typeof import("./config"); +const { writeGlobalConfig } = (await import(configModuleSpecifier)) as typeof import('./config'); const originalPlatform = process.platform; -const testConfigPath = "/tmp/dt-skill-config-test/config.json"; +const testConfigPath = '/tmp/dt-skill-config-test/config.json'; const envStubs = createEnvStubRegistry(); function makeErr(code: string): NodeJS.ErrnoException { - const error = new Error(code) as NodeJS.ErrnoException; - error.code = code; - return error; + const error = new Error(code) as NodeJS.ErrnoException; + error.code = code; + return error; } beforeEach(() => { - envStubs.stub("DT_SKILL_CONFIG_PATH", testConfigPath); - Object.defineProperty(process, "platform", { value: "linux" }); - fsMocks.chmod.mockResolvedValue(undefined); - fsMocks.mkdir.mockResolvedValue(undefined); - fsMocks.readFile.mockResolvedValue(""); - fsMocks.writeFile.mockResolvedValue(undefined); + envStubs.stub('DT_SKILL_CONFIG_PATH', testConfigPath); + Object.defineProperty(process, 'platform', { value: 'linux' }); + fsMocks.chmod.mockResolvedValue(undefined); + fsMocks.mkdir.mockResolvedValue(undefined); + fsMocks.readFile.mockResolvedValue(''); + fsMocks.writeFile.mockResolvedValue(undefined); }); afterEach(() => { - Object.defineProperty(process, "platform", { value: originalPlatform }); - envStubs.restoreAll(); - vi.clearAllMocks(); - fsMocks.chmod.mockReset(); - fsMocks.mkdir.mockReset(); - fsMocks.readFile.mockReset(); - fsMocks.writeFile.mockReset(); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + envStubs.restoreAll(); + vi.clearAllMocks(); + fsMocks.chmod.mockReset(); + fsMocks.mkdir.mockReset(); + fsMocks.readFile.mockReset(); + fsMocks.writeFile.mockReset(); }); -describe("writeGlobalConfig", () => { - it("writes config with restricted modes", async () => { - await writeGlobalConfig({ registry: "https://example.com" }); +describe('writeGlobalConfig', () => { + it('writes config with restricted modes', async () => { + await writeGlobalConfig({ registry: 'https://example.com' }); - expect(fsMocks.mkdir).toHaveBeenCalledWith("/tmp/dt-skill-config-test", { - recursive: true, - mode: 0o700, + expect(fsMocks.mkdir).toHaveBeenCalledWith('/tmp/dt-skill-config-test', { + recursive: true, + mode: 0o700, + }); + expect(fsMocks.writeFile).toHaveBeenCalledWith( + testConfigPath, + expect.stringContaining('"registry": "https://example.com"'), + { + encoding: 'utf8', + mode: 0o600, + } + ); + expect(fsMocks.chmod).toHaveBeenCalledWith(testConfigPath, 0o600); }); - expect(fsMocks.writeFile).toHaveBeenCalledWith( - testConfigPath, - expect.stringContaining('"registry": "https://example.com"'), - { - encoding: "utf8", - mode: 0o600, - }, - ); - expect(fsMocks.chmod).toHaveBeenCalledWith(testConfigPath, 0o600); - }); - it("ignores non-fatal chmod errors", async () => { - fsMocks.chmod.mockRejectedValueOnce(makeErr("ENOTSUP")); + it('ignores non-fatal chmod errors', async () => { + fsMocks.chmod.mockRejectedValueOnce(makeErr('ENOTSUP')); - await expect(writeGlobalConfig({ registry: "https://example.com" })).resolves.toBeUndefined(); - }); + await expect( + writeGlobalConfig({ registry: 'https://example.com' }) + ).resolves.toBeUndefined(); + }); - it("rethrows unexpected chmod errors", async () => { - fsMocks.chmod.mockRejectedValueOnce(new Error("boom")); + it('rethrows unexpected chmod errors', async () => { + fsMocks.chmod.mockRejectedValueOnce(new Error('boom')); - await expect(writeGlobalConfig({ registry: "https://example.com" })).rejects.toThrow("boom"); - }); + await expect(writeGlobalConfig({ registry: 'https://example.com' })).rejects.toThrow( + 'boom' + ); + }); }); diff --git a/dt-skill/src/config.ts b/dt-skill/src/config.ts index 70205269..f95b32a1 100644 --- a/dt-skill/src/config.ts +++ b/dt-skill/src/config.ts @@ -1,72 +1,72 @@ -import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; -import { resolveHome } from "./homedir.js"; -import { type GlobalConfig, GlobalConfigSchema, parseArk } from "./schema/index.js"; +import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { resolveHome } from './homedir.js'; +import { type GlobalConfig, GlobalConfigSchema, parseArk } from './schema/index.js'; function resolveConfigPath(baseDir: string): string { - return join(baseDir, "dt-skill", "config.json"); + return join(baseDir, 'dt-skill', 'config.json'); } function isNonFatalChmodError(error: unknown): boolean { - if (!(error instanceof Error)) return false; - const code = (error as NodeJS.ErrnoException).code; - return code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EINVAL"; + if (!(error instanceof Error)) return false; + const code = (error as NodeJS.ErrnoException).code; + return code === 'EPERM' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EINVAL'; } function getGlobalConfigPath() { - const override = process.env.DT_SKILL_CONFIG_PATH?.trim(); - if (override) return resolve(override); + const override = process.env.DT_SKILL_CONFIG_PATH?.trim(); + if (override) return resolve(override); - const home = resolveHome(); + const home = resolveHome(); - if (process.platform === "darwin") { - return resolveConfigPath(join(home, "Library", "Application Support")); - } + if (process.platform === 'darwin') { + return resolveConfigPath(join(home, 'Library', 'Application Support')); + } - const xdg = process.env.XDG_CONFIG_HOME; - if (xdg) { - return resolveConfigPath(xdg); - } + const xdg = process.env.XDG_CONFIG_HOME; + if (xdg) { + return resolveConfigPath(xdg); + } - if (process.platform === "win32") { - const appData = process.env.APPDATA; - if (appData) { - return resolveConfigPath(appData); + if (process.platform === 'win32') { + const appData = process.env.APPDATA; + if (appData) { + return resolveConfigPath(appData); + } } - } - return resolveConfigPath(join(home, ".config")); + return resolveConfigPath(join(home, '.config')); } export async function readGlobalConfig(): Promise { - try { - const raw = await readFile(getGlobalConfigPath(), "utf8"); - const parsed = JSON.parse(raw) as unknown; - return parseArk(GlobalConfigSchema, parsed, "Global config"); - } catch { - return null; - } + try { + const raw = await readFile(getGlobalConfigPath(), 'utf8'); + const parsed = JSON.parse(raw) as unknown; + return parseArk(GlobalConfigSchema, parsed, 'Global config'); + } catch { + return null; + } } export async function writeGlobalConfig(config: GlobalConfig) { - const path = getGlobalConfigPath(); - const dir = dirname(path); + const path = getGlobalConfigPath(); + const dir = dirname(path); - // Create directory with restricted permissions (owner only) - await mkdir(dir, { recursive: true, mode: 0o700 }); + // Create directory with restricted permissions (owner only) + await mkdir(dir, { recursive: true, mode: 0o700 }); - // Write file with restricted permissions (owner read/write only) - await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { - encoding: "utf8", - mode: 0o600, - }); + // Write file with restricted permissions (owner read/write only) + await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { + encoding: 'utf8', + mode: 0o600, + }); - // Ensure permissions on existing files (writeFile mode only applies on create) - if (process.platform !== "win32") { - try { - await chmod(path, 0o600); - } catch (error) { - if (!isNonFatalChmodError(error)) throw error; + // Ensure permissions on existing files (writeFile mode only applies on create) + if (process.platform !== 'win32') { + try { + await chmod(path, 0o600); + } catch (error) { + if (!isNonFatalChmodError(error)) throw error; + } } - } } diff --git a/dt-skill/src/discovery.test.ts b/dt-skill/src/discovery.test.ts index c9fa9c26..85518834 100644 --- a/dt-skill/src/discovery.test.ts +++ b/dt-skill/src/discovery.test.ts @@ -1,77 +1,77 @@ /* @vitest-environment node */ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { createGlobalStubRegistry } from "../test/runtimeStubs.js"; -import { discoverRegistryFromSite } from "./discovery"; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { createGlobalStubRegistry } from '../test/runtimeStubs.js'; +import { discoverRegistryFromSite } from './discovery'; const globalStubs = createGlobalStubRegistry(); -describe("discovery", () => { - afterEach(() => { - globalStubs.restoreAll(); - vi.restoreAllMocks(); - vi.clearAllMocks(); - }); +describe('discovery', () => { + afterEach(() => { + globalStubs.restoreAll(); + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); - it("returns null on non-ok response", async () => { - globalStubs.stub( - "fetch", - vi.fn(async () => new Response("nope", { status: 404 })) as unknown as typeof fetch, - ); - await expect(discoverRegistryFromSite("https://example.com")).resolves.toBeNull(); - }); + it('returns null on non-ok response', async () => { + globalStubs.stub( + 'fetch', + vi.fn(async () => new Response('nope', { status: 404 })) as unknown as typeof fetch + ); + await expect(discoverRegistryFromSite('https://example.com')).resolves.toBeNull(); + }); - it("parses registry config", async () => { - globalStubs.stub( - "fetch", - vi.fn( - async () => - new Response(JSON.stringify({ registry: "https://example.convex.site" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ) as unknown as typeof fetch, - ); - await expect(discoverRegistryFromSite("https://example.com")).resolves.toEqual({ - apiBase: "https://example.convex.site", - minCliVersion: undefined, + it('parses registry config', async () => { + globalStubs.stub( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ registry: 'https://example.convex.site' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) as unknown as typeof fetch + ); + await expect(discoverRegistryFromSite('https://example.com')).resolves.toEqual({ + apiBase: 'https://example.convex.site', + minCliVersion: undefined, + }); }); - }); - it("parses apiBase config", async () => { - globalStubs.stub( - "fetch", - vi.fn( - async () => - new Response( - JSON.stringify({ - apiBase: "https://api.example.com", - minCliVersion: "1.2.3", - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ), - ) as unknown as typeof fetch, - ); - await expect(discoverRegistryFromSite("https://example.com")).resolves.toEqual({ - apiBase: "https://api.example.com", - minCliVersion: "1.2.3", + it('parses apiBase config', async () => { + globalStubs.stub( + 'fetch', + vi.fn( + async () => + new Response( + JSON.stringify({ + apiBase: 'https://api.example.com', + minCliVersion: '1.2.3', + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ) + ) as unknown as typeof fetch + ); + await expect(discoverRegistryFromSite('https://example.com')).resolves.toEqual({ + apiBase: 'https://api.example.com', + minCliVersion: '1.2.3', + }); }); - }); - it("returns null when apiBase is empty", async () => { - globalStubs.stub( - "fetch", - vi.fn( - async () => - new Response(JSON.stringify({ apiBase: "" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - ) as unknown as typeof fetch, - ); - await expect(discoverRegistryFromSite("https://example.com")).resolves.toBeNull(); - }); + it('returns null when apiBase is empty', async () => { + globalStubs.stub( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ apiBase: '' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) as unknown as typeof fetch + ); + await expect(discoverRegistryFromSite('https://example.com')).resolves.toBeNull(); + }); }); diff --git a/dt-skill/src/discovery.ts b/dt-skill/src/discovery.ts index dccf7bb9..b7af6f0a 100644 --- a/dt-skill/src/discovery.ts +++ b/dt-skill/src/discovery.ts @@ -1,18 +1,18 @@ -import { parseArk, WellKnownConfigSchema } from "./schema/index.js"; +import { parseArk, WellKnownConfigSchema } from './schema/index.js'; export async function discoverRegistryFromSite(siteUrl: string) { - const url = new URL("/.well-known/dt-skill.json", siteUrl); - const response = await fetch(url.toString(), { - method: "GET", - headers: { Accept: "application/json" }, - }); - if (!response.ok) return null; - const raw = (await response.json()) as unknown; - const parsed = parseArk(WellKnownConfigSchema, raw, "WellKnown config"); - const apiBase = "apiBase" in parsed ? parsed.apiBase : parsed.registry; - if (!apiBase) return null; - return { - apiBase, - minCliVersion: parsed.minCliVersion, - }; + const url = new URL('/.well-known/dt-skill.json', siteUrl); + const response = await fetch(url.toString(), { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + if (!response.ok) return null; + const raw = (await response.json()) as unknown; + const parsed = parseArk(WellKnownConfigSchema, raw, 'WellKnown config'); + const apiBase = 'apiBase' in parsed ? parsed.apiBase : parsed.registry; + if (!apiBase) return null; + return { + apiBase, + minCliVersion: parsed.minCliVersion, + }; } diff --git a/dt-skill/src/homedir.ts b/dt-skill/src/homedir.ts index eea260d7..6bcf0d2e 100644 --- a/dt-skill/src/homedir.ts +++ b/dt-skill/src/homedir.ts @@ -1,5 +1,5 @@ -import { homedir } from "node:os"; -import { win32 } from "node:path"; +import { homedir } from 'node:os'; +import { win32 } from 'node:path'; /** * Resolve the user's home directory, preferring environment variables over @@ -8,22 +8,24 @@ import { win32 } from "node:path"; * is resolved from the current runtime environment. */ export function resolveHome(): string { - if (process.platform === "win32") { - return normalizeHome(process.env.USERPROFILE) || normalizeHome(process.env.HOME) || homedir(); - } - return normalizeHome(process.env.HOME) || homedir(); + if (process.platform === 'win32') { + return ( + normalizeHome(process.env.USERPROFILE) || normalizeHome(process.env.HOME) || homedir() + ); + } + return normalizeHome(process.env.HOME) || homedir(); } function normalizeHome(value: string | undefined): string { - const trimmed = value?.trim(); - if (!trimmed) return ""; + const trimmed = value?.trim(); + if (!trimmed) return ''; - if (process.platform === "win32") { - const root = win32.parse(trimmed).root; - if (trimmed === root) return trimmed; - return trimmed.replace(/[\\/]+$/, ""); - } + if (process.platform === 'win32') { + const root = win32.parse(trimmed).root; + if (trimmed === root) return trimmed; + return trimmed.replace(/[\\/]+$/, ''); + } - if (trimmed === "/") return "/"; - return trimmed.replace(/\/+$/, ""); + if (trimmed === '/') return '/'; + return trimmed.replace(/\/+$/, ''); } diff --git a/dt-skill/src/http.bun.test.ts b/dt-skill/src/http.bun.test.ts index a3f89229..12ce6fa7 100644 --- a/dt-skill/src/http.bun.test.ts +++ b/dt-skill/src/http.bun.test.ts @@ -1,188 +1,190 @@ /* @vitest-environment node */ -import { describe, expect, it, vi } from "vitest"; -import { createHttpClient } from "./http.js"; +import { describe, expect, it, vi } from 'vitest'; +import { createHttpClient } from './http.js'; type SpawnResult = { - status: number | null; - stdout?: string; - stderr?: string; + status: number | null; + stdout?: string; + stderr?: string; }; function createBunClient(options?: { - spawnImpl?: (...args: unknown[]) => SpawnResult; - mkdtempValue?: string; - readFileValue?: Buffer | null; + spawnImpl?: (...args: unknown[]) => SpawnResult; + mkdtempValue?: string; + readFileValue?: Buffer | null; }) { - const spawnImpl = vi.fn(options?.spawnImpl ?? (() => ({ status: 0, stdout: "", stderr: "" }))); - const mkdirImpl = vi.fn(async () => undefined); - const mkdtempImpl = vi.fn(async () => options?.mkdtempValue ?? "/tmp/dt-skill-test"); - const rmImpl = vi.fn(async () => undefined); - const writeFileImpl = vi.fn(async () => undefined); - const readFileImpl = vi.fn( - async () => (options?.readFileValue ?? Buffer.from([1, 2, 3])) as Buffer, - ); - const setTimeoutImpl = vi.fn((callback: () => void, _ms?: number) => { - callback(); - return 1 as unknown as ReturnType; - }); - const clearTimeoutImpl = vi.fn(); - - return { - client: createHttpClient({ - runtime: "bun", - configureDispatcher: false, - spawnSyncImpl: spawnImpl as unknown as typeof import("node:child_process").spawnSync, - mkdirImpl: mkdirImpl as unknown as typeof import("node:fs/promises").mkdir, - mkdtempImpl: mkdtempImpl as unknown as typeof import("node:fs/promises").mkdtemp, - rmImpl: rmImpl as unknown as typeof import("node:fs/promises").rm, - writeFileImpl: writeFileImpl as unknown as typeof import("node:fs/promises").writeFile, - readFileImpl: readFileImpl as unknown as typeof import("node:fs/promises").readFile, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, - tmpdirPath: "/tmp", - random: () => 0, - }), - spawnImpl, - mkdirImpl, - mkdtempImpl, - rmImpl, - writeFileImpl, - readFileImpl, - setTimeoutImpl, - clearTimeoutImpl, - }; -} - -describe("bun http client", () => { - it("uses curl for apiRequest GET and POST", async () => { - const { client, spawnImpl } = createBunClient({ - spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: "" }), - }); - - const getResult = await client.apiRequest<{ ok: boolean }>("https://registry.example", { - method: "GET", - path: "/v1/ping", - }); - await client.apiRequest("https://registry.example", { - method: "POST", - path: "/v1/ping", - body: { a: 1 }, - }); - - expect(getResult).toEqual({ ok: true }); - const [, getArgs] = spawnImpl.mock.calls[0] as [string, string[]]; - expect(getArgs).toContain("GET"); - expect(getArgs).toContain("https://registry.example/v1/ping"); - expect(getArgs.some((arg) => arg.startsWith("Authorization: Bearer"))).toBe(false); - - const [, postArgs] = spawnImpl.mock.calls[1] as [string, string[]]; - expect(postArgs).toContain("Content-Type: application/json"); - expect(postArgs).toContain("--data-binary"); - expect(postArgs).toContain('{"a":1}'); - }); - - it("retries 429 responses and keeps 404 non-retryable", async () => { - const rateLimited = createBunClient({ - spawnImpl: () => ({ status: 0, stdout: "rate limited\n429", stderr: "" }), - }); - - await expect( - rateLimited.client.apiRequest("https://registry.example", { - method: "GET", - path: "/v1/ping", - }), - ).rejects.toThrow("rate limited"); - expect(rateLimited.spawnImpl).toHaveBeenCalledTimes(3); - - const missing = createBunClient({ - spawnImpl: () => ({ status: 0, stdout: "missing\n404", stderr: "" }), - }); - await expect( - missing.client.apiRequest("https://registry.example", { - method: "GET", - path: "/v1/ping", - }), - ).rejects.toThrow("missing"); - expect(missing.spawnImpl).toHaveBeenCalledTimes(1); - }); - - it("includes curl rate-limit metadata in 429 errors", async () => { - const { client, spawnImpl } = createBunClient({ - spawnImpl: () => ({ - status: 0, - stdout: "rate limited\n__DT_SKILL_CURL_META__\n429\n20\n0\n1771404540\n20\n0\n34\n34\n", - stderr: "", - }), + const spawnImpl = vi.fn(options?.spawnImpl ?? (() => ({ status: 0, stdout: '', stderr: '' }))); + const mkdirImpl = vi.fn(async () => undefined); + const mkdtempImpl = vi.fn(async () => options?.mkdtempValue ?? '/tmp/dt-skill-test'); + const rmImpl = vi.fn(async () => undefined); + const writeFileImpl = vi.fn(async () => undefined); + const readFileImpl = vi.fn( + async () => (options?.readFileValue ?? Buffer.from([1, 2, 3])) as Buffer + ); + const setTimeoutImpl = vi.fn((callback: () => void, _ms?: number) => { + callback(); + return 1 as unknown as ReturnType; }); + const clearTimeoutImpl = vi.fn(); + + return { + client: createHttpClient({ + runtime: 'bun', + configureDispatcher: false, + spawnSyncImpl: spawnImpl as unknown as typeof import('node:child_process').spawnSync, + mkdirImpl: mkdirImpl as unknown as typeof import('node:fs/promises').mkdir, + mkdtempImpl: mkdtempImpl as unknown as typeof import('node:fs/promises').mkdtemp, + rmImpl: rmImpl as unknown as typeof import('node:fs/promises').rm, + writeFileImpl: writeFileImpl as unknown as typeof import('node:fs/promises').writeFile, + readFileImpl: readFileImpl as unknown as typeof import('node:fs/promises').readFile, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + tmpdirPath: '/tmp', + random: () => 0, + }), + spawnImpl, + mkdirImpl, + mkdtempImpl, + rmImpl, + writeFileImpl, + readFileImpl, + setTimeoutImpl, + clearTimeoutImpl, + }; +} - await expect( - client.apiRequest("https://registry.example", { - method: "GET", - path: "/v1/ping", - }), - ).rejects.toThrow(/retry in 34s.*remaining: 0\/20.*reset in 34s/i); - expect(spawnImpl).toHaveBeenCalledTimes(3); - }); - - it("supports fetchText and downloadZip via curl", async () => { - const { client, spawnImpl, readFileImpl, rmImpl } = createBunClient({ - spawnImpl: vi - .fn() - .mockReturnValueOnce({ status: 0, stdout: "hello world\n200", stderr: "" }) - .mockReturnValueOnce({ status: 0, stdout: "200", stderr: "" }) - .mockReturnValueOnce({ status: 0, stdout: "404", stderr: "" }), - mkdtempValue: "/tmp/dt-skill-download-abc", - readFileValue: Buffer.from("not found"), +describe('bun http client', () => { + it('uses curl for apiRequest GET and POST', async () => { + const { client, spawnImpl } = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: '' }), + }); + + const getResult = await client.apiRequest<{ ok: boolean }>('https://registry.example', { + method: 'GET', + path: '/v1/ping', + }); + await client.apiRequest('https://registry.example', { + method: 'POST', + path: '/v1/ping', + body: { a: 1 }, + }); + + expect(getResult).toEqual({ ok: true }); + const [, getArgs] = spawnImpl.mock.calls[0] as [string, string[]]; + expect(getArgs).toContain('GET'); + expect(getArgs).toContain('https://registry.example/v1/ping'); + expect(getArgs.some((arg) => arg.startsWith('Authorization: Bearer'))).toBe(false); + + const [, postArgs] = spawnImpl.mock.calls[1] as [string, string[]]; + expect(postArgs).toContain('Content-Type: application/json'); + expect(postArgs).toContain('--data-binary'); + expect(postArgs).toContain('{"a":1}'); }); - await expect( - client.fetchText("https://registry.example", { path: "/v1/readme" }), - ).resolves.toBe("hello world"); - const bytes = await client.downloadZip("https://registry.example", { - slug: "demo", - }); - expect(Array.from(bytes)).toEqual(Array.from(Buffer.from("not found"))); - await expect( - client.downloadZip("https://registry.example", { slug: "demo" }), - ).rejects.toThrow("not found"); - - expect(readFileImpl).toHaveBeenCalled(); - expect(rmImpl).toHaveBeenCalledWith("/tmp/dt-skill-download-abc", { - recursive: true, - force: true, + it('retries 429 responses and keeps 404 non-retryable', async () => { + const rateLimited = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: 'rate limited\n429', stderr: '' }), + }); + + await expect( + rateLimited.client.apiRequest('https://registry.example', { + method: 'GET', + path: '/v1/ping', + }) + ).rejects.toThrow('rate limited'); + expect(rateLimited.spawnImpl).toHaveBeenCalledTimes(3); + + const missing = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: 'missing\n404', stderr: '' }), + }); + await expect( + missing.client.apiRequest('https://registry.example', { + method: 'GET', + path: '/v1/ping', + }) + ).rejects.toThrow('missing'); + expect(missing.spawnImpl).toHaveBeenCalledTimes(1); }); - expect(spawnImpl).toHaveBeenCalledTimes(3); - }); - it("posts multipart form data via curl and cleans up temp files", async () => { - const { client, spawnImpl, mkdirImpl, writeFileImpl, rmImpl } = createBunClient({ - spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: "" }), - mkdtempValue: "/tmp/dt-skill-upload-abc", + it('includes curl rate-limit metadata in 429 errors', async () => { + const { client, spawnImpl } = createBunClient({ + spawnImpl: () => ({ + status: 0, + stdout: 'rate limited\n__DT_SKILL_CURL_META__\n429\n20\n0\n1771404540\n20\n0\n34\n34\n', + stderr: '', + }), + }); + + await expect( + client.apiRequest('https://registry.example', { + method: 'GET', + path: '/v1/ping', + }) + ).rejects.toThrow(/retry in 34s.*remaining: 0\/20.*reset in 34s/i); + expect(spawnImpl).toHaveBeenCalledTimes(3); }); - const form = new FormData(); - form.append("name", "demo"); - form.append("file", new Blob(["abc"], { type: "text/plain" }), "dist/demo.txt"); - - const result = await client.apiRequestForm<{ ok: boolean }>("https://registry.example", { - method: "POST", - path: "/upload", - form, + it('supports fetchText and downloadZip via curl', async () => { + const { client, spawnImpl, readFileImpl, rmImpl } = createBunClient({ + spawnImpl: vi + .fn() + .mockReturnValueOnce({ status: 0, stdout: 'hello world\n200', stderr: '' }) + .mockReturnValueOnce({ status: 0, stdout: '200', stderr: '' }) + .mockReturnValueOnce({ status: 0, stdout: '404', stderr: '' }), + mkdtempValue: '/tmp/dt-skill-download-abc', + readFileValue: Buffer.from('not found'), + }); + + await expect( + client.fetchText('https://registry.example', { path: '/v1/readme' }) + ).resolves.toBe('hello world'); + const bytes = await client.downloadZip('https://registry.example', { + slug: 'demo', + }); + expect(Array.from(bytes)).toEqual(Array.from(Buffer.from('not found'))); + await expect( + client.downloadZip('https://registry.example', { slug: 'demo' }) + ).rejects.toThrow('not found'); + + expect(readFileImpl).toHaveBeenCalled(); + expect(rmImpl).toHaveBeenCalledWith('/tmp/dt-skill-download-abc', { + recursive: true, + force: true, + }); + expect(spawnImpl).toHaveBeenCalledTimes(3); }); - expect(result).toEqual({ ok: true }); - expect(mkdirImpl).toHaveBeenCalledWith("/tmp/dt-skill-upload-abc/dist", { recursive: true }); - expect(writeFileImpl).toHaveBeenCalled(); - expect(rmImpl).toHaveBeenCalledWith("/tmp/dt-skill-upload-abc", { - recursive: true, - force: true, + it('posts multipart form data via curl and cleans up temp files', async () => { + const { client, spawnImpl, mkdirImpl, writeFileImpl, rmImpl } = createBunClient({ + spawnImpl: () => ({ status: 0, stdout: '{"ok":true}\n200', stderr: '' }), + mkdtempValue: '/tmp/dt-skill-upload-abc', + }); + + const form = new FormData(); + form.append('name', 'demo'); + form.append('file', new Blob(['abc'], { type: 'text/plain' }), 'dist/demo.txt'); + + const result = await client.apiRequestForm<{ ok: boolean }>('https://registry.example', { + method: 'POST', + path: '/upload', + form, + }); + + expect(result).toEqual({ ok: true }); + expect(mkdirImpl).toHaveBeenCalledWith('/tmp/dt-skill-upload-abc/dist', { + recursive: true, + }); + expect(writeFileImpl).toHaveBeenCalled(); + expect(rmImpl).toHaveBeenCalledWith('/tmp/dt-skill-upload-abc', { + recursive: true, + force: true, + }); + const [, args] = spawnImpl.mock.calls[0] as [string, string[]]; + expect(args).toContain('-F'); + expect(args.some((arg) => arg.includes('name=demo'))).toBe(true); + expect( + args.some((arg) => arg.includes('file=@/tmp/dt-skill-upload-abc/dist/demo.txt')) + ).toBe(true); }); - const [, args] = spawnImpl.mock.calls[0] as [string, string[]]; - expect(args).toContain("-F"); - expect(args.some((arg) => arg.includes("name=demo"))).toBe(true); - expect(args.some((arg) => arg.includes("file=@/tmp/dt-skill-upload-abc/dist/demo.txt"))).toBe( - true, - ); - }); }); diff --git a/dt-skill/src/http.test.ts b/dt-skill/src/http.test.ts index 6ccba019..a702f619 100644 --- a/dt-skill/src/http.test.ts +++ b/dt-skill/src/http.test.ts @@ -1,408 +1,418 @@ /* @vitest-environment node */ -import { describe, expect, it, vi } from "vitest"; -import { createHttpClient, detectHttpRuntime, registryUrl, shouldUseProxyFromEnv } from "./http.js"; +import { describe, expect, it, vi } from 'vitest'; +import { createHttpClient, detectHttpRuntime, registryUrl, shouldUseProxyFromEnv } from './http.js'; function createNodeClient(options?: { - fetchImpl?: typeof fetch; - setTimeoutImpl?: typeof setTimeout; - clearTimeoutImpl?: typeof clearTimeout; - now?: () => number; + fetchImpl?: typeof fetch; + setTimeoutImpl?: typeof setTimeout; + clearTimeoutImpl?: typeof clearTimeout; + now?: () => number; }) { - return createHttpClient({ - runtime: "node", - configureDispatcher: false, - fetchImpl: options?.fetchImpl, - setTimeoutImpl: options?.setTimeoutImpl, - clearTimeoutImpl: options?.clearTimeoutImpl, - now: options?.now, - random: () => 0, - }); + return createHttpClient({ + runtime: 'node', + configureDispatcher: false, + fetchImpl: options?.fetchImpl, + setTimeoutImpl: options?.setTimeoutImpl, + clearTimeoutImpl: options?.clearTimeoutImpl, + now: options?.now, + random: () => 0, + }); } function createImmediateTimeouts() { - const setTimeoutImpl = vi.fn((callback: () => void, _ms?: number) => { - callback(); - return 1 as unknown as ReturnType; - }); - const clearTimeoutImpl = vi.fn(); - return { setTimeoutImpl, clearTimeoutImpl }; + const setTimeoutImpl = vi.fn((callback: () => void, _ms?: number) => { + callback(); + return 1 as unknown as ReturnType; + }); + const clearTimeoutImpl = vi.fn(); + return { setTimeoutImpl, clearTimeoutImpl }; } function createAbortingFetchMock() { - return vi.fn(async (_url: string, init?: RequestInit) => { - const signal = init?.signal; - if (!(signal instanceof AbortSignal)) { - throw new Error("Missing abort signal"); - } - if (signal.aborted) { - throw signal.reason; - } - return await new Promise((_resolve, reject) => { - signal.addEventListener( - "abort", - () => { - reject(signal.reason); - }, - { once: true }, - ); + return vi.fn(async (_url: string, init?: RequestInit) => { + const signal = init?.signal; + if (!(signal instanceof AbortSignal)) { + throw new Error('Missing abort signal'); + } + if (signal.aborted) { + throw signal.reason; + } + return await new Promise((_resolve, reject) => { + signal.addEventListener( + 'abort', + () => { + reject(signal.reason); + }, + { once: true } + ); + }); }); - }); } -describe("detectHttpRuntime", () => { - it("detects bun and node runtimes explicitly", () => { - expect(detectHttpRuntime({ bun: "1.2.3" } as unknown as NodeJS.ProcessVersions)).toBe("bun"); - expect(detectHttpRuntime({ node: "22.0.0" } as unknown as NodeJS.ProcessVersions)).toBe("node"); - }); +describe('detectHttpRuntime', () => { + it('detects bun and node runtimes explicitly', () => { + expect(detectHttpRuntime({ bun: '1.2.3' } as unknown as NodeJS.ProcessVersions)).toBe( + 'bun' + ); + expect(detectHttpRuntime({ node: '22.0.0' } as unknown as NodeJS.ProcessVersions)).toBe( + 'node' + ); + }); }); -describe("shouldUseProxyFromEnv", () => { - it("detects standard proxy variables", () => { - expect( - shouldUseProxyFromEnv({ - HTTPS_PROXY: "http://proxy.example:3128", - } as NodeJS.ProcessEnv), - ).toBe(true); - expect( - shouldUseProxyFromEnv({ - HTTP_PROXY: "http://proxy.example:3128", - } as NodeJS.ProcessEnv), - ).toBe(true); - expect( - shouldUseProxyFromEnv({ - https_proxy: "http://proxy.example:3128", - } as NodeJS.ProcessEnv), - ).toBe(true); - }); - - it("ignores NO_PROXY-only configs", () => { - expect( - shouldUseProxyFromEnv({ - NO_PROXY: "localhost,127.0.0.1", - } as NodeJS.ProcessEnv), - ).toBe(false); - expect(shouldUseProxyFromEnv({} as NodeJS.ProcessEnv)).toBe(false); - }); -}); +describe('shouldUseProxyFromEnv', () => { + it('detects standard proxy variables', () => { + expect( + shouldUseProxyFromEnv({ + HTTPS_PROXY: 'http://proxy.example:3128', + } as NodeJS.ProcessEnv) + ).toBe(true); + expect( + shouldUseProxyFromEnv({ + HTTP_PROXY: 'http://proxy.example:3128', + } as NodeJS.ProcessEnv) + ).toBe(true); + expect( + shouldUseProxyFromEnv({ + https_proxy: 'http://proxy.example:3128', + } as NodeJS.ProcessEnv) + ).toBe(true); + }); -describe("registryUrl", () => { - it("preserves registry base paths and normalizes slashes", () => { - expect(registryUrl("/api/v1/skills", "https://example.com").toString()).toBe( - "https://example.com/api/v1/skills", - ); - expect(registryUrl("/api/v1/skills", "http://localhost:8081/custom/path").toString()).toBe( - "http://localhost:8081/custom/path/api/v1/skills", - ); - expect(registryUrl("api/v1/skills", "http://localhost:8081/custom/path/").toString()).toBe( - "http://localhost:8081/custom/path/api/v1/skills", - ); - }); + it('ignores NO_PROXY-only configs', () => { + expect( + shouldUseProxyFromEnv({ + NO_PROXY: 'localhost,127.0.0.1', + } as NodeJS.ProcessEnv) + ).toBe(false); + expect(shouldUseProxyFromEnv({} as NodeJS.ProcessEnv)).toBe(false); + }); }); -describe("node http client", () => { - it("parses json without adding authorization", async () => { - const fetchImpl = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ user: { handle: null } }), +describe('registryUrl', () => { + it('preserves registry base paths and normalizes slashes', () => { + expect(registryUrl('/api/v1/skills', 'https://example.com').toString()).toBe( + 'https://example.com/api/v1/skills' + ); + expect(registryUrl('/api/v1/skills', 'http://localhost:8081/custom/path').toString()).toBe( + 'http://localhost:8081/custom/path/api/v1/skills' + ); + expect(registryUrl('api/v1/skills', 'http://localhost:8081/custom/path/').toString()).toBe( + 'http://localhost:8081/custom/path/api/v1/skills' + ); }); - const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); +}); - const result = await client.apiRequest<{ user: { handle: null } }>("https://example.com", { - method: "GET", - path: "/x", +describe('node http client', () => { + it('parses json without adding authorization', async () => { + const fetchImpl = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ user: { handle: null } }), + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + const result = await client.apiRequest<{ user: { handle: null } }>('https://example.com', { + method: 'GET', + path: '/x', + }); + + expect(result.user.handle).toBeNull(); + const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; + expect((init.headers as Record).Authorization).toBeUndefined(); }); - expect(result.user.handle).toBeNull(); - const [, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; - expect((init.headers as Record).Authorization).toBeUndefined(); - }); - - it("posts json body", async () => { - const fetchImpl = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ ok: true }), + it('posts json body', async () => { + const fetchImpl = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ ok: true }), + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + await client.apiRequest('https://example.com', { + method: 'POST', + path: '/x', + body: { a: 1 }, + }); + + const [url, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; + expect(url).toBe('https://example.com/x'); + expect(init.body).toBe(JSON.stringify({ a: 1 })); + expect((init.headers as Record)['Content-Type']).toBe('application/json'); }); - const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); - await client.apiRequest("https://example.com", { - method: "POST", - path: "/x", - body: { a: 1 }, + it('includes rate-limit guidance from response headers on 429', async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + headers: new Headers({ + 'Retry-After': '34', + 'X-RateLimit-Limit': '20', + 'X-RateLimit-Remaining': '0', + 'X-RateLimit-Reset': '1771404540', + }), + text: async () => 'Rate limit exceeded', + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/x' }) + ).rejects.toThrow(/retry in 34s.*remaining: 0\/20.*reset in 34s/i); + expect(fetchImpl).toHaveBeenCalledTimes(3); + expect(clearTimeoutImpl).toHaveBeenCalledTimes(3); }); - const [url, init] = fetchImpl.mock.calls[0] as [string, RequestInit]; - expect(url).toBe("https://example.com/x"); - expect(init.body).toBe(JSON.stringify({ a: 1 })); - expect((init.headers as Record)["Content-Type"]).toBe("application/json"); - }); - - it("includes rate-limit guidance from response headers on 429", async () => { - const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); - const fetchImpl = vi.fn().mockResolvedValue({ - ok: false, - status: 429, - headers: new Headers({ - "Retry-After": "34", - "X-RateLimit-Limit": "20", - "X-RateLimit-Remaining": "0", - "X-RateLimit-Reset": "1771404540", - }), - text: async () => "Rate limit exceeded", - }); - const client = createNodeClient({ - fetchImpl: fetchImpl as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, + it('interprets legacy epoch Retry-After values as reset delays', async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + headers: new Headers({ + 'Retry-After': '1771404540', + 'X-RateLimit-Limit': '20', + 'X-RateLimit-Remaining': '0', + }), + text: async () => 'Rate limit exceeded', + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + now: () => 1_771_404_500_000, + }); + + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/x' }) + ).rejects.toThrow(/retry in 40s.*remaining: 0\/20/i); }); - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/x" }), - ).rejects.toThrow(/retry in 34s.*remaining: 0\/20.*reset in 34s/i); - expect(fetchImpl).toHaveBeenCalledTimes(3); - expect(clearTimeoutImpl).toHaveBeenCalledTimes(3); - }); - - it("interprets legacy epoch Retry-After values as reset delays", async () => { - const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); - const fetchImpl = vi.fn().mockResolvedValue({ - ok: false, - status: 429, - headers: new Headers({ - "Retry-After": "1771404540", - "X-RateLimit-Limit": "20", - "X-RateLimit-Remaining": "0", - }), - text: async () => "Rate limit exceeded", - }); - const client = createNodeClient({ - fetchImpl: fetchImpl as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, - now: () => 1_771_404_500_000, + it('falls back to HTTP status when response bodies are empty', async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + text: async () => '', + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequest('https://example.com', { + method: 'GET', + url: 'https://example.com/x', + }) + ).rejects.toThrow('HTTP 500'); + expect(fetchImpl).toHaveBeenCalledTimes(3); }); - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/x" }), - ).rejects.toThrow(/retry in 40s.*remaining: 0\/20/i); - }); - - it("falls back to HTTP status when response bodies are empty", async () => { - const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); - const fetchImpl = vi.fn().mockResolvedValue({ - ok: false, - status: 500, - text: async () => "", - }); - const client = createNodeClient({ - fetchImpl: fetchImpl as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, + it('retries and labels transient Convex write contention', async () => { + const contention = + 'Documents read from or written to the "publishers" table changed while this mutation was being run'; + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 400, + text: async () => contention, + }) + .mockResolvedValueOnce({ + ok: false, + status: 400, + text: async () => contention, + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ ok: true }), + }); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequestForm('https://example.com', { + method: 'POST', + path: '/upload', + form: new FormData(), + retryCount: 5, + }) + ).resolves.toEqual({ ok: true }); + expect(fetchImpl).toHaveBeenCalledTimes(3); + + const failingFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 400, + text: async () => contention, + }); + const failingClient = createNodeClient({ + fetchImpl: failingFetch as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + await expect( + failingClient.apiRequestForm('https://example.com', { + method: 'POST', + path: '/upload', + form: new FormData(), + retryCount: 0, + }) + ).rejects.toThrow(/Transient ClawHub write contention.*package artifact passed/i); }); - await expect( - client.apiRequest("https://example.com", { method: "GET", url: "https://example.com/x" }), - ).rejects.toThrow("HTTP 500"); - expect(fetchImpl).toHaveBeenCalledTimes(3); - }); - - it("retries and labels transient Convex write contention", async () => { - const contention = - 'Documents read from or written to the "publishers" table changed while this mutation was being run'; - const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); - const fetchImpl = vi - .fn() - .mockResolvedValueOnce({ - ok: false, - status: 400, - text: async () => contention, - }) - .mockResolvedValueOnce({ - ok: false, - status: 400, - text: async () => contention, - }) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ ok: true }), - }); - const client = createNodeClient({ - fetchImpl: fetchImpl as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, + it('expands generic auth and visibility failures into actionable messages', async () => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce({ + ok: false, + status: 401, + headers: new Headers(), + text: async () => 'Unauthorized', + }) + .mockResolvedValueOnce({ + ok: false, + status: 403, + headers: new Headers(), + text: async () => 'Forbidden', + }) + .mockResolvedValueOnce({ + ok: false, + status: 404, + headers: new Headers(), + text: async () => 'Package not found', + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/auth' }) + ).rejects.toThrow(/authentication required/i); + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/forbidden' }) + ).rejects.toThrow(/account does not have access.*not in good standing/i); + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/missing' }) + ).rejects.toThrow(/Package not found or not visible to this account/i); }); - await expect( - client.apiRequestForm("https://example.com", { - method: "POST", - path: "/upload", - form: new FormData(), - retryCount: 5, - }), - ).resolves.toEqual({ ok: true }); - expect(fetchImpl).toHaveBeenCalledTimes(3); - - const failingFetch = vi.fn().mockResolvedValue({ - ok: false, - status: 400, - text: async () => contention, - }); - const failingClient = createNodeClient({ - fetchImpl: failingFetch as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, - }); - await expect( - failingClient.apiRequestForm("https://example.com", { - method: "POST", - path: "/upload", - form: new FormData(), - retryCount: 0, - }), - ).rejects.toThrow(/Transient ClawHub write contention.*package artifact passed/i); - }); - - it("expands generic auth and visibility failures into actionable messages", async () => { - const fetchImpl = vi - .fn() - .mockResolvedValueOnce({ - ok: false, - status: 401, - headers: new Headers(), - text: async () => "Unauthorized", - }) - .mockResolvedValueOnce({ - ok: false, - status: 403, - headers: new Headers(), - text: async () => "Forbidden", - }) - .mockResolvedValueOnce({ - ok: false, - status: 404, - headers: new Headers(), - text: async () => "Package not found", - }); - const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); - - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/auth" }), - ).rejects.toThrow(/authentication required/i); - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/forbidden" }), - ).rejects.toThrow(/account does not have access.*not in good standing/i); - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/missing" }), - ).rejects.toThrow(/Package not found or not visible to this account/i); - }); - - it("downloads zip bytes and does not retry non-retryable errors", async () => { - const fetchImpl = vi - .fn() - .mockResolvedValueOnce({ - ok: true, - arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, - }) - .mockResolvedValueOnce({ - ok: false, - status: 404, - text: async () => "nope", - }); - const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); - - const bytes = await client.downloadZip("https://example.com", { - slug: "demo", - version: "1.0.0", - }); - expect(Array.from(bytes)).toEqual([1, 2, 3]); - - await expect(client.downloadZip("https://example.com", { slug: "demo" })).rejects.toThrow( - "nope", - ); - expect(fetchImpl).toHaveBeenCalledTimes(2); - }); - - it("retries request and text timeouts using injected timeout helpers", async () => { - const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); - const fetchImpl = createAbortingFetchMock(); - const client = createNodeClient({ - fetchImpl: fetchImpl as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, + it('downloads zip bytes and does not retry non-retryable errors', async () => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, + }) + .mockResolvedValueOnce({ + ok: false, + status: 404, + text: async () => 'nope', + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); + + const bytes = await client.downloadZip('https://example.com', { + slug: 'demo', + version: '1.0.0', + }); + expect(Array.from(bytes)).toEqual([1, 2, 3]); + + await expect(client.downloadZip('https://example.com', { slug: 'demo' })).rejects.toThrow( + 'nope' + ); + expect(fetchImpl).toHaveBeenCalledTimes(2); }); - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/x" }), - ).rejects.toThrow(/timed out/i); - await expect(client.fetchText("https://example.com", { path: "/x" })).rejects.toThrow( - /timed out/i, - ); - expect(fetchImpl).toHaveBeenCalledTimes(6); - expect(clearTimeoutImpl).toHaveBeenCalledTimes(6); - }); - - it("normalizes non-Error throws from fetch", async () => { - const fetchImpl = vi.fn(async () => { - throw { message: "The operation was aborted", name: "AbortError" }; + it('retries request and text timeouts using injected timeout helpers', async () => { + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const fetchImpl = createAbortingFetchMock(); + const client = createNodeClient({ + fetchImpl: fetchImpl as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/x' }) + ).rejects.toThrow(/timed out/i); + await expect(client.fetchText('https://example.com', { path: '/x' })).rejects.toThrow( + /timed out/i + ); + expect(fetchImpl).toHaveBeenCalledTimes(6); + expect(clearTimeoutImpl).toHaveBeenCalledTimes(6); }); - const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); - await expect( - client.apiRequest("https://example.com", { method: "GET", path: "/x" }), - ).rejects.toThrow("The operation was aborted"); - }); + it('normalizes non-Error throws from fetch', async () => { + const fetchImpl = vi.fn(async () => { + throw { message: 'The operation was aborted', name: 'AbortError' }; + }); + const client = createNodeClient({ fetchImpl: fetchImpl as unknown as typeof fetch }); - it("posts form data, retries 429, and uses the upload timeout", async () => { - const successFetch = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ ok: true }), + await expect( + client.apiRequest('https://example.com', { method: 'GET', path: '/x' }) + ).rejects.toThrow('The operation was aborted'); }); - const successClient = createNodeClient({ fetchImpl: successFetch as unknown as typeof fetch }); - const form = new FormData(); - form.append("x", "1"); - const result = await successClient.apiRequestForm("https://example.com", { - method: "POST", - path: "/upload", - form, - }); - expect(result).toEqual({ ok: true }); - const [, init] = successFetch.mock.calls[0] as [string, RequestInit]; - expect(init.body).toBe(form); - expect((init.headers as Record).Authorization).toBeUndefined(); - - const rateLimitedFetch = vi.fn().mockResolvedValue({ - ok: false, - status: 429, - text: async () => "rate limited", - }); - const retryClient = createNodeClient({ - fetchImpl: rateLimitedFetch as unknown as typeof fetch, - setTimeoutImpl: createImmediateTimeouts().setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl: vi.fn(), - }); - await expect( - retryClient.apiRequestForm("https://example.com", { - method: "POST", - path: "/upload", - form: new FormData(), - }), - ).rejects.toThrow("rate limited"); - expect(rateLimitedFetch).toHaveBeenCalledTimes(3); - - const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); - const abortingFetch = createAbortingFetchMock(); - const timeoutClient = createNodeClient({ - fetchImpl: abortingFetch as unknown as typeof fetch, - setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, - clearTimeoutImpl, + + it('posts form data, retries 429, and uses the upload timeout', async () => { + const successFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ ok: true }), + }); + const successClient = createNodeClient({ + fetchImpl: successFetch as unknown as typeof fetch, + }); + const form = new FormData(); + form.append('x', '1'); + const result = await successClient.apiRequestForm('https://example.com', { + method: 'POST', + path: '/upload', + form, + }); + expect(result).toEqual({ ok: true }); + const [, init] = successFetch.mock.calls[0] as [string, RequestInit]; + expect(init.body).toBe(form); + expect((init.headers as Record).Authorization).toBeUndefined(); + + const rateLimitedFetch = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + text: async () => 'rate limited', + }); + const retryClient = createNodeClient({ + fetchImpl: rateLimitedFetch as unknown as typeof fetch, + setTimeoutImpl: createImmediateTimeouts() + .setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl: vi.fn(), + }); + await expect( + retryClient.apiRequestForm('https://example.com', { + method: 'POST', + path: '/upload', + form: new FormData(), + }) + ).rejects.toThrow('rate limited'); + expect(rateLimitedFetch).toHaveBeenCalledTimes(3); + + const { setTimeoutImpl, clearTimeoutImpl } = createImmediateTimeouts(); + const abortingFetch = createAbortingFetchMock(); + const timeoutClient = createNodeClient({ + fetchImpl: abortingFetch as unknown as typeof fetch, + setTimeoutImpl: setTimeoutImpl as unknown as typeof setTimeout, + clearTimeoutImpl, + }); + await expect( + timeoutClient.apiRequestForm('https://example.com', { + method: 'POST', + path: '/upload', + form: new FormData(), + }) + ).rejects.toThrow(/timed out after 120s/i); + expect(setTimeoutImpl.mock.calls[0]?.[1]).toBe(120_000); }); - await expect( - timeoutClient.apiRequestForm("https://example.com", { - method: "POST", - path: "/upload", - form: new FormData(), - }), - ).rejects.toThrow(/timed out after 120s/i); - expect(setTimeoutImpl.mock.calls[0]?.[1]).toBe(120_000); - }); }); diff --git a/dt-skill/src/http.ts b/dt-skill/src/http.ts index 8780eb0b..6b8dc969 100644 --- a/dt-skill/src/http.ts +++ b/dt-skill/src/http.ts @@ -1,11 +1,11 @@ -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import pRetry, { AbortError } from "p-retry"; -import { Agent, EnvHttpProxyAgent, setGlobalDispatcher } from "undici"; -import type { ArkValidator } from "./schema/index.js"; -import { ApiRoutes, parseArk } from "./schema/index.js"; +import { spawnSync } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import pRetry, { AbortError } from 'p-retry'; +import { Agent, EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'; +import type { ArkValidator } from './schema/index.js'; +import { ApiRoutes, parseArk } from './schema/index.js'; const REQUEST_TIMEOUT_MS = 15_000; const UPLOAD_TIMEOUT_MS = 120_000; @@ -15,800 +15,804 @@ const RETRY_COUNT = 2; const RETRY_BACKOFF_BASE_MS = 300; const RETRY_BACKOFF_MAX_MS = 5_000; const RETRY_AFTER_JITTER_MS = 250; -const CURL_META_MARKER = "__DT_SKILL_CURL_META__"; +const CURL_META_MARKER = '__DT_SKILL_CURL_META__'; const CURL_WRITE_OUT_FORMAT = [ - "", - CURL_META_MARKER, - "%{http_code}", - "%{header:x-ratelimit-limit}", - "%{header:x-ratelimit-remaining}", - "%{header:x-ratelimit-reset}", - "%{header:ratelimit-limit}", - "%{header:ratelimit-remaining}", - "%{header:ratelimit-reset}", - "%{header:retry-after}", -].join("\n"); - -export type HttpRuntime = "node" | "bun"; + '', + CURL_META_MARKER, + '%{http_code}', + '%{header:x-ratelimit-limit}', + '%{header:x-ratelimit-remaining}', + '%{header:x-ratelimit-reset}', + '%{header:ratelimit-limit}', + '%{header:ratelimit-remaining}', + '%{header:ratelimit-reset}', + '%{header:retry-after}', +].join('\n'); + +export type HttpRuntime = 'node' | 'bun'; type RequestArgs = - | { - method: "GET" | "POST" | "DELETE"; - path: string; - body?: unknown; - retryCount?: number; - } - | { - method: "GET" | "POST" | "DELETE"; - url: string; - body?: unknown; - retryCount?: number; - }; + | { + method: 'GET' | 'POST' | 'DELETE'; + path: string; + body?: unknown; + retryCount?: number; + } + | { + method: 'GET' | 'POST' | 'DELETE'; + url: string; + body?: unknown; + retryCount?: number; + }; type FormRequestArgs = - | { method: "POST"; path: string; form: FormData; retryCount?: number } - | { method: "POST"; url: string; form: FormData; retryCount?: number }; + | { method: 'POST'; path: string; form: FormData; retryCount?: number } + | { method: 'POST'; url: string; form: FormData; retryCount?: number }; type TextRequestArgs = { path: string } | { url: string }; type HeaderSource = Headers | Record | null | undefined; type RateLimitInfo = { - limit?: number; - remaining?: number; - resetDelaySeconds?: number; - retryAfterSeconds?: number; + limit?: number; + remaining?: number; + resetDelaySeconds?: number; + retryAfterSeconds?: number; }; type HttpClientDeps = { - runtime: HttpRuntime; - fetchImpl: typeof fetch; - setTimeoutImpl: typeof setTimeout; - clearTimeoutImpl: typeof clearTimeout; - spawnSyncImpl: typeof spawnSync; - mkdirImpl: typeof mkdir; - mkdtempImpl: typeof mkdtemp; - readFileImpl: typeof readFile; - rmImpl: typeof rm; - writeFileImpl: typeof writeFile; - tmpdirPath: string; - now: () => number; - random: () => number; - env: NodeJS.ProcessEnv; - configureDispatcher: boolean; + runtime: HttpRuntime; + fetchImpl: typeof fetch; + setTimeoutImpl: typeof setTimeout; + clearTimeoutImpl: typeof clearTimeout; + spawnSyncImpl: typeof spawnSync; + mkdirImpl: typeof mkdir; + mkdtempImpl: typeof mkdtemp; + readFileImpl: typeof readFile; + rmImpl: typeof rm; + writeFileImpl: typeof writeFile; + tmpdirPath: string; + now: () => number; + random: () => number; + env: NodeJS.ProcessEnv; + configureDispatcher: boolean; }; -export type HttpClientOptions = Partial> & { - runtime?: HttpRuntime; +export type HttpClientOptions = Partial> & { + runtime?: HttpRuntime; }; type HttpClient = { - apiRequest(registry: string, args: RequestArgs): Promise; - apiRequest(registry: string, args: RequestArgs, schema: ArkValidator): Promise; - apiRequestForm(registry: string, args: FormRequestArgs): Promise; - apiRequestForm(registry: string, args: FormRequestArgs, schema: ArkValidator): Promise; - fetchText(registry: string, args: TextRequestArgs): Promise; - fetchBinary(registry: string, args: TextRequestArgs): Promise; - downloadZip( - registry: string, - args: { slug: string; version?: string }, - ): Promise; + apiRequest(registry: string, args: RequestArgs): Promise; + apiRequest(registry: string, args: RequestArgs, schema: ArkValidator): Promise; + apiRequestForm(registry: string, args: FormRequestArgs): Promise; + apiRequestForm(registry: string, args: FormRequestArgs, schema: ArkValidator): Promise; + fetchText(registry: string, args: TextRequestArgs): Promise; + fetchBinary(registry: string, args: TextRequestArgs): Promise; + downloadZip(registry: string, args: { slug: string; version?: string }): Promise; }; class HttpStatusError extends Error { - readonly status: number; - readonly rateLimit: RateLimitInfo; - - constructor(status: number, message: string, rateLimit: RateLimitInfo) { - super(message); - this.name = "HttpStatusError"; - this.status = status; - this.rateLimit = rateLimit; - } + readonly status: number; + readonly rateLimit: RateLimitInfo; + + constructor(status: number, message: string, rateLimit: RateLimitInfo) { + super(message); + this.name = 'HttpStatusError'; + this.status = status; + this.rateLimit = rateLimit; + } } export function detectHttpRuntime( - processVersions: NodeJS.ProcessVersions | undefined = process.versions, + processVersions: NodeJS.ProcessVersions | undefined = process.versions ): HttpRuntime { - return processVersions?.bun ? "bun" : "node"; + return processVersions?.bun ? 'bun' : 'node'; } export function shouldUseProxyFromEnv(env: NodeJS.ProcessEnv = process.env): boolean { - return Boolean(env.HTTPS_PROXY || env.HTTP_PROXY || env.https_proxy || env.http_proxy); + return Boolean(env.HTTPS_PROXY || env.HTTP_PROXY || env.https_proxy || env.http_proxy); } export function registryUrl(path: string, registry: string): URL { - const base = registry.endsWith("/") ? registry : `${registry}/`; - const relative = path.startsWith("/") ? path.slice(1) : path; - return new URL(relative, base); + const base = registry.endsWith('/') ? registry : `${registry}/`; + const relative = path.startsWith('/') ? path.slice(1) : path; + return new URL(relative, base); } export function createHttpClient(options: HttpClientOptions = {}): HttpClient { - const deps: HttpClientDeps = { - runtime: options.runtime ?? detectHttpRuntime(), - fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis), - setTimeoutImpl: options.setTimeoutImpl ?? globalThis.setTimeout.bind(globalThis), - clearTimeoutImpl: options.clearTimeoutImpl ?? globalThis.clearTimeout.bind(globalThis), - spawnSyncImpl: options.spawnSyncImpl ?? spawnSync, - mkdirImpl: options.mkdirImpl ?? mkdir, - mkdtempImpl: options.mkdtempImpl ?? mkdtemp, - readFileImpl: options.readFileImpl ?? readFile, - rmImpl: options.rmImpl ?? rm, - writeFileImpl: options.writeFileImpl ?? writeFile, - tmpdirPath: options.tmpdirPath ?? tmpdir(), - now: options.now ?? Date.now, - random: options.random ?? Math.random, - env: options.env ?? process.env, - configureDispatcher: options.configureDispatcher ?? true, - }; - - if (deps.runtime === "node" && deps.configureDispatcher) { - configureNodeDispatcher(deps.env); - } - - const runWithRetries = createRetryRunner(deps); - - async function apiRequest( - registry: string, - args: RequestArgs, - schema?: ArkValidator, - ): Promise { - const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); - const json = await runWithRetries(async () => { - if (deps.runtime === "bun") { - return await fetchJsonViaCurl(deps, url, args); - } - - const headers: Record = { Accept: "application/json" }; - let body: string | undefined; - if (args.body !== undefined || args.method === "POST") { - headers["Content-Type"] = "application/json"; - body = JSON.stringify(args.body ?? {}); - } - const response = await fetchWithTimeout(deps, url, { - method: args.method, - headers, - body, - }); - if (!response.ok) { - throwHttpStatusError( - response.status, - await readResponseTextSafe(response), - response.headers, - deps.now, - ); - } - return (await response.json()) as unknown; - }, args.retryCount); - if (schema) return parseArk(schema, json, "API response"); - return json as T; - } - - async function apiRequestForm( - registry: string, - args: FormRequestArgs, - schema?: ArkValidator, - ): Promise { - const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); - const json = await runWithRetries(async () => { - if (deps.runtime === "bun") { - return await fetchJsonFormViaCurl(deps, url, args); - } + const deps: HttpClientDeps = { + runtime: options.runtime ?? detectHttpRuntime(), + fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis), + setTimeoutImpl: options.setTimeoutImpl ?? globalThis.setTimeout.bind(globalThis), + clearTimeoutImpl: options.clearTimeoutImpl ?? globalThis.clearTimeout.bind(globalThis), + spawnSyncImpl: options.spawnSyncImpl ?? spawnSync, + mkdirImpl: options.mkdirImpl ?? mkdir, + mkdtempImpl: options.mkdtempImpl ?? mkdtemp, + readFileImpl: options.readFileImpl ?? readFile, + rmImpl: options.rmImpl ?? rm, + writeFileImpl: options.writeFileImpl ?? writeFile, + tmpdirPath: options.tmpdirPath ?? tmpdir(), + now: options.now ?? Date.now, + random: options.random ?? Math.random, + env: options.env ?? process.env, + configureDispatcher: options.configureDispatcher ?? true, + }; - const headers: Record = { Accept: "application/json" }; - const response = await fetchWithTimeout( - deps, - url, - { - method: args.method, - headers, - body: args.form, - }, - UPLOAD_TIMEOUT_MS, - ); - if (!response.ok) { - throwHttpStatusError( - response.status, - await readResponseTextSafe(response), - response.headers, - deps.now, - ); - } - return (await response.json()) as unknown; - }, args.retryCount); - if (schema) return parseArk(schema, json, "API response"); - return json as T; - } - - async function fetchTextRequest(registry: string, args: TextRequestArgs): Promise { - const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); - return await runWithRetries(async () => { - if (deps.runtime === "bun") { - return await fetchTextViaCurl(deps, url); - } + if (deps.runtime === 'node' && deps.configureDispatcher) { + configureNodeDispatcher(deps.env); + } - const headers: Record = { Accept: "text/plain" }; - const response = await fetchWithTimeout(deps, url, { method: "GET", headers }); - const text = await response.text(); - if (!response.ok) { - throwHttpStatusError(response.status, text, response.headers, deps.now); - } - return text; - }); - } + const runWithRetries = createRetryRunner(deps); + + async function apiRequest( + registry: string, + args: RequestArgs, + schema?: ArkValidator + ): Promise { + const url = 'url' in args ? args.url : registryUrl(args.path, registry).toString(); + const json = await runWithRetries(async () => { + if (deps.runtime === 'bun') { + return await fetchJsonViaCurl(deps, url, args); + } + + const headers: Record = { Accept: 'application/json' }; + let body: string | undefined; + if (args.body !== undefined || args.method === 'POST') { + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(args.body ?? {}); + } + const response = await fetchWithTimeout(deps, url, { + method: args.method, + headers, + body, + }); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now + ); + } + return (await response.json()) as unknown; + }, args.retryCount); + if (schema) return parseArk(schema, json, 'API response'); + return json as T; + } - async function fetchBinaryRequest(registry: string, args: TextRequestArgs): Promise { - const url = "url" in args ? args.url : registryUrl(args.path, registry).toString(); - return await runWithRetries(async () => { - if (deps.runtime === "bun") { - return await fetchBinaryViaCurl(deps, url); - } + async function apiRequestForm( + registry: string, + args: FormRequestArgs, + schema?: ArkValidator + ): Promise { + const url = 'url' in args ? args.url : registryUrl(args.path, registry).toString(); + const json = await runWithRetries(async () => { + if (deps.runtime === 'bun') { + return await fetchJsonFormViaCurl(deps, url, args); + } + + const headers: Record = { Accept: 'application/json' }; + const response = await fetchWithTimeout( + deps, + url, + { + method: args.method, + headers, + body: args.form, + }, + UPLOAD_TIMEOUT_MS + ); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now + ); + } + return (await response.json()) as unknown; + }, args.retryCount); + if (schema) return parseArk(schema, json, 'API response'); + return json as T; + } - const headers: Record = {}; - const response = await fetchWithTimeout(deps, url, { method: "GET", headers }); - if (!response.ok) { - throwHttpStatusError( - response.status, - await readResponseTextSafe(response), - response.headers, - deps.now, - ); - } - return new Uint8Array(await response.arrayBuffer()); - }); - } + async function fetchTextRequest(registry: string, args: TextRequestArgs): Promise { + const url = 'url' in args ? args.url : registryUrl(args.path, registry).toString(); + return await runWithRetries(async () => { + if (deps.runtime === 'bun') { + return await fetchTextViaCurl(deps, url); + } + + const headers: Record = { Accept: 'text/plain' }; + const response = await fetchWithTimeout(deps, url, { method: 'GET', headers }); + const text = await response.text(); + if (!response.ok) { + throwHttpStatusError(response.status, text, response.headers, deps.now); + } + return text; + }); + } - async function downloadZipRequest( - registry: string, - args: { slug: string; version?: string }, - ) { - const url = registryUrl(ApiRoutes.download, registry); - url.searchParams.set("slug", args.slug); - if (args.version) url.searchParams.set("version", args.version); - return await runWithRetries(async () => { - if (deps.runtime === "bun") { - return await fetchBinaryViaCurl(deps, url.toString()); - } + async function fetchBinaryRequest( + registry: string, + args: TextRequestArgs + ): Promise { + const url = 'url' in args ? args.url : registryUrl(args.path, registry).toString(); + return await runWithRetries(async () => { + if (deps.runtime === 'bun') { + return await fetchBinaryViaCurl(deps, url); + } + + const headers: Record = {}; + const response = await fetchWithTimeout(deps, url, { method: 'GET', headers }); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now + ); + } + return new Uint8Array(await response.arrayBuffer()); + }); + } - const headers: Record = {}; - const response = await fetchWithTimeout(deps, url.toString(), { method: "GET", headers }); - if (!response.ok) { - throwHttpStatusError( - response.status, - await readResponseTextSafe(response), - response.headers, - deps.now, - ); - } - return new Uint8Array(await response.arrayBuffer()); - }); - } + async function downloadZipRequest(registry: string, args: { slug: string; version?: string }) { + const url = registryUrl(ApiRoutes.download, registry); + url.searchParams.set('slug', args.slug); + if (args.version) url.searchParams.set('version', args.version); + return await runWithRetries(async () => { + if (deps.runtime === 'bun') { + return await fetchBinaryViaCurl(deps, url.toString()); + } + + const headers: Record = {}; + const response = await fetchWithTimeout(deps, url.toString(), { + method: 'GET', + headers, + }); + if (!response.ok) { + throwHttpStatusError( + response.status, + await readResponseTextSafe(response), + response.headers, + deps.now + ); + } + return new Uint8Array(await response.arrayBuffer()); + }); + } - return { - apiRequest, - apiRequestForm, - fetchText: fetchTextRequest, - fetchBinary: fetchBinaryRequest, - downloadZip: downloadZipRequest, - }; + return { + apiRequest, + apiRequestForm, + fetchText: fetchTextRequest, + fetchBinary: fetchBinaryRequest, + downloadZip: downloadZipRequest, + }; } function configureNodeDispatcher(env: NodeJS.ProcessEnv) { - if (!process.versions?.node) return; - try { - setGlobalDispatcher( - shouldUseProxyFromEnv(env) - ? new EnvHttpProxyAgent({ - connect: { timeout: REQUEST_TIMEOUT_MS }, - }) - : new Agent({ - connect: { timeout: REQUEST_TIMEOUT_MS }, - }), - ); - } catch { - // Ignore dispatcher setup failures in environments that partially emulate Node APIs. - } + if (!process.versions?.node) return; + try { + setGlobalDispatcher( + shouldUseProxyFromEnv(env) + ? new EnvHttpProxyAgent({ + connect: { timeout: REQUEST_TIMEOUT_MS }, + }) + : new Agent({ + connect: { timeout: REQUEST_TIMEOUT_MS }, + }) + ); + } catch { + // Ignore dispatcher setup failures in environments that partially emulate Node APIs. + } } const defaultHttpClient = createHttpClient(); export async function apiRequest(registry: string, args: RequestArgs): Promise; export async function apiRequest( - registry: string, - args: RequestArgs, - schema: ArkValidator, + registry: string, + args: RequestArgs, + schema: ArkValidator ): Promise; export async function apiRequest( - registry: string, - args: RequestArgs, - schema?: ArkValidator, + registry: string, + args: RequestArgs, + schema?: ArkValidator ): Promise { - if (schema) { - return await defaultHttpClient.apiRequest(registry, args, schema); - } - return await defaultHttpClient.apiRequest(registry, args); + if (schema) { + return await defaultHttpClient.apiRequest(registry, args, schema); + } + return await defaultHttpClient.apiRequest(registry, args); } export async function apiRequestForm(registry: string, args: FormRequestArgs): Promise; export async function apiRequestForm( - registry: string, - args: FormRequestArgs, - schema: ArkValidator, + registry: string, + args: FormRequestArgs, + schema: ArkValidator ): Promise; export async function apiRequestForm( - registry: string, - args: FormRequestArgs, - schema?: ArkValidator, + registry: string, + args: FormRequestArgs, + schema?: ArkValidator ): Promise { - if (schema) { - return await defaultHttpClient.apiRequestForm(registry, args, schema); - } - return await defaultHttpClient.apiRequestForm(registry, args); + if (schema) { + return await defaultHttpClient.apiRequestForm(registry, args, schema); + } + return await defaultHttpClient.apiRequestForm(registry, args); } export async function fetchText(registry: string, args: TextRequestArgs): Promise { - return await defaultHttpClient.fetchText(registry, args); + return await defaultHttpClient.fetchText(registry, args); } export async function fetchBinary(registry: string, args: TextRequestArgs): Promise { - return await defaultHttpClient.fetchBinary(registry, args); -} - -export async function downloadZip( - registry: string, - args: { slug: string; version?: string }, -) { - return await defaultHttpClient.downloadZip(registry, args); -} - -function createRetryRunner(deps: Pick) { - return async function runWithRetries( - fn: () => Promise, - retryCount = RETRY_COUNT, - ): Promise { - return await pRetry(fn, { - retries: retryCount, - minTimeout: 0, - maxTimeout: 0, - factor: 1, - randomize: false, - onFailedAttempt: async (attemptError) => { - const delayMs = getRetryDelayMs(attemptError, deps.random); - if (delayMs <= 0) return; - await sleep(delayMs, deps.setTimeoutImpl); - }, - }); - }; + return await defaultHttpClient.fetchBinary(registry, args); +} + +export async function downloadZip(registry: string, args: { slug: string; version?: string }) { + return await defaultHttpClient.downloadZip(registry, args); +} + +function createRetryRunner(deps: Pick) { + return async function runWithRetries( + fn: () => Promise, + retryCount = RETRY_COUNT + ): Promise { + return await pRetry(fn, { + retries: retryCount, + minTimeout: 0, + maxTimeout: 0, + factor: 1, + randomize: false, + onFailedAttempt: async (attemptError) => { + const delayMs = getRetryDelayMs(attemptError, deps.random); + if (delayMs <= 0) return; + await sleep(delayMs, deps.setTimeoutImpl); + }, + }); + }; } async function fetchWithTimeout( - deps: Pick, - url: string, - init: RequestInit, - timeoutMs = REQUEST_TIMEOUT_MS, + deps: Pick, + url: string, + init: RequestInit, + timeoutMs = REQUEST_TIMEOUT_MS ): Promise { - const controller = new AbortController(); - const timeoutSeconds = Math.ceil(timeoutMs / 1000); - const timeout = deps.setTimeoutImpl( - () => controller.abort(new Error(`Request timed out after ${timeoutSeconds}s`)), - timeoutMs, - ); - try { - return await deps.fetchImpl(url, { ...init, signal: controller.signal }); - } catch (error) { - if (error instanceof Error) throw error; - const message = - typeof error === "object" && error !== null && "message" in error - ? String((error as { message: unknown }).message) - : String(error); - throw new Error(message, { cause: error }); - } finally { - deps.clearTimeoutImpl(timeout); - } + const controller = new AbortController(); + const timeoutSeconds = Math.ceil(timeoutMs / 1000); + const timeout = deps.setTimeoutImpl( + () => controller.abort(new Error(`Request timed out after ${timeoutSeconds}s`)), + timeoutMs + ); + try { + return await deps.fetchImpl(url, { ...init, signal: controller.signal }); + } catch (error) { + if (error instanceof Error) throw error; + const message = + typeof error === 'object' && error !== null && 'message' in error + ? String((error as { message: unknown }).message) + : String(error); + throw new Error(message, { cause: error }); + } finally { + deps.clearTimeoutImpl(timeout); + } } async function readResponseTextSafe(response: Response): Promise { - return await response.text().catch(() => ""); + return await response.text().catch(() => ''); } function getRetryDelayMs(attemptError: unknown, random: () => number): number { - const failed = attemptError as { - attemptNumber?: number; - cause?: unknown; - error?: unknown; - }; - const attemptNumber = Math.max(1, failed.attemptNumber ?? 1); - const rootError = failed.cause ?? failed.error ?? attemptError; - if (rootError instanceof HttpStatusError && rootError.rateLimit.retryAfterSeconds !== undefined) { - return rootError.rateLimit.retryAfterSeconds * 1000 + jitterMs(RETRY_AFTER_JITTER_MS, random); - } - const baseMs = Math.min(RETRY_BACKOFF_MAX_MS, RETRY_BACKOFF_BASE_MS * 2 ** (attemptNumber - 1)); - return baseMs + jitterMs(RETRY_BACKOFF_BASE_MS, random); + const failed = attemptError as { + attemptNumber?: number; + cause?: unknown; + error?: unknown; + }; + const attemptNumber = Math.max(1, failed.attemptNumber ?? 1); + const rootError = failed.cause ?? failed.error ?? attemptError; + if ( + rootError instanceof HttpStatusError && + rootError.rateLimit.retryAfterSeconds !== undefined + ) { + return ( + rootError.rateLimit.retryAfterSeconds * 1000 + jitterMs(RETRY_AFTER_JITTER_MS, random) + ); + } + const baseMs = Math.min(RETRY_BACKOFF_MAX_MS, RETRY_BACKOFF_BASE_MS * 2 ** (attemptNumber - 1)); + return baseMs + jitterMs(RETRY_BACKOFF_BASE_MS, random); } function sleep(ms: number, setTimeoutImpl: typeof setTimeout): Promise { - return new Promise((resolve) => { - setTimeoutImpl(resolve, ms); - }); + return new Promise((resolve) => { + setTimeoutImpl(resolve, ms); + }); } function jitterMs(maxMs: number, random: () => number): number { - if (maxMs <= 0) return 0; - return Math.floor(random() * maxMs); + if (maxMs <= 0) return 0; + return Math.floor(random() * maxMs); } function throwHttpStatusError( - status: number, - text: string, - headers: HeaderSource, - now: () => number, + status: number, + text: string, + headers: HeaderSource, + now: () => number ): never { - const rateLimit = parseRateLimitInfo(headers, now); - const retryableTransientContention = isTransientConvexContention(text); - const message = buildHttpErrorMessage(status, text, rateLimit); - if (status === 429 || status >= 500 || retryableTransientContention) { - throw new HttpStatusError(status, message, rateLimit); - } - throw new AbortError(message); + const rateLimit = parseRateLimitInfo(headers, now); + const retryableTransientContention = isTransientConvexContention(text); + const message = buildHttpErrorMessage(status, text, rateLimit); + if (status === 429 || status >= 500 || retryableTransientContention) { + throw new HttpStatusError(status, message, rateLimit); + } + throw new AbortError(message); } function buildHttpErrorMessage(status: number, text: string, rateLimit: RateLimitInfo): string { - const base = normalizeHttpErrorBody(status, text); - const details: string[] = []; - if (rateLimit.retryAfterSeconds !== undefined) { - details.push(`retry in ${rateLimit.retryAfterSeconds}s`); - } - if (rateLimit.remaining !== undefined && rateLimit.limit !== undefined) { - details.push(`remaining: ${rateLimit.remaining}/${rateLimit.limit}`); - } - if (rateLimit.resetDelaySeconds !== undefined) { - details.push(`reset in ${rateLimit.resetDelaySeconds}s`); - } - return details.length === 0 ? base : `${base} (${details.join(", ")})`; + const base = normalizeHttpErrorBody(status, text); + const details: string[] = []; + if (rateLimit.retryAfterSeconds !== undefined) { + details.push(`retry in ${rateLimit.retryAfterSeconds}s`); + } + if (rateLimit.remaining !== undefined && rateLimit.limit !== undefined) { + details.push(`remaining: ${rateLimit.remaining}/${rateLimit.limit}`); + } + if (rateLimit.resetDelaySeconds !== undefined) { + details.push(`reset in ${rateLimit.resetDelaySeconds}s`); + } + return details.length === 0 ? base : `${base} (${details.join(', ')})`; } function normalizeHttpErrorBody(status: number, text: string): string { - const body = text.trim(); - const lowered = body.toLowerCase(); - if (body && lowered !== "unauthorized" && lowered !== "forbidden") { - if (isTransientConvexContention(body)) { - return `Transient ClawHub write contention. The package artifact passed request validation; retrying usually succeeds. ${body}`; + const body = text.trim(); + const lowered = body.toLowerCase(); + if (body && lowered !== 'unauthorized' && lowered !== 'forbidden') { + if (isTransientConvexContention(body)) { + return `Transient ClawHub write contention. The package artifact passed request validation; retrying usually succeeds. ${body}`; + } + if (status === 404 && lowered === 'package not found') { + return 'Package not found or not visible to this account.'; + } + if (status === 404 && lowered === 'skill not found') { + return 'Skill not found or unavailable to this account.'; + } + return body; } - if (status === 404 && lowered === "package not found") { - return "Package not found or not visible to this account."; + if (status === 401) { + return 'Authentication required for this operation.'; } - if (status === 404 && lowered === "skill not found") { - return "Skill not found or unavailable to this account."; + if (status === 403) { + return 'Permission denied. This account does not have access to this operation, or the account is not in good standing.'; } - return body; - } - if (status === 401) { - return "Authentication required for this operation."; - } - if (status === 403) { - return "Permission denied. This account does not have access to this operation, or the account is not in good standing."; - } - if (body) return body; - return `HTTP ${status}`; + if (body) return body; + return `HTTP ${status}`; } function isTransientConvexContention(text: string) { - const lowered = text.toLowerCase(); - return ( - lowered.includes("optimistic concurrency") || - lowered.includes("write conflict") || - (lowered.includes('documents read from or written to the "') && - lowered.includes("changed while this mutation was being run")) - ); + const lowered = text.toLowerCase(); + return ( + lowered.includes('optimistic concurrency') || + lowered.includes('write conflict') || + (lowered.includes('documents read from or written to the "') && + lowered.includes('changed while this mutation was being run')) + ); } function parseRateLimitInfo(headers: HeaderSource, now: () => number): RateLimitInfo { - if (!headers) return {}; - const limit = parseIntHeader( - getHeader(headers, "x-ratelimit-limit") ?? getHeader(headers, "ratelimit-limit"), - ); - const remaining = parseIntHeader( - getHeader(headers, "x-ratelimit-remaining") ?? getHeader(headers, "ratelimit-remaining"), - ); - const nowMs = now(); - const retryAfterSeconds = parseRetryAfterSeconds(getHeader(headers, "retry-after"), nowMs); - const resetDelaySeconds = parseResetDelaySeconds(headers, nowMs, retryAfterSeconds); - return { limit, remaining, resetDelaySeconds, retryAfterSeconds }; + if (!headers) return {}; + const limit = parseIntHeader( + getHeader(headers, 'x-ratelimit-limit') ?? getHeader(headers, 'ratelimit-limit') + ); + const remaining = parseIntHeader( + getHeader(headers, 'x-ratelimit-remaining') ?? getHeader(headers, 'ratelimit-remaining') + ); + const nowMs = now(); + const retryAfterSeconds = parseRetryAfterSeconds(getHeader(headers, 'retry-after'), nowMs); + const resetDelaySeconds = parseResetDelaySeconds(headers, nowMs, retryAfterSeconds); + return { limit, remaining, resetDelaySeconds, retryAfterSeconds }; } function parseResetDelaySeconds( - headers: HeaderSource, - nowMs: number, - retryAfterSeconds: number | undefined, + headers: HeaderSource, + nowMs: number, + retryAfterSeconds: number | undefined ): number | undefined { - if (retryAfterSeconds !== undefined) return retryAfterSeconds; - const standardized = parseIntHeader(getHeader(headers, "ratelimit-reset")); - if (standardized !== undefined) { - return Math.max(1, standardized); - } - const legacyEpochSeconds = parseIntHeader(getHeader(headers, "x-ratelimit-reset")); - if (legacyEpochSeconds === undefined) return undefined; - const nowSeconds = Math.floor(nowMs / 1000); - return Math.max(1, legacyEpochSeconds - nowSeconds); + if (retryAfterSeconds !== undefined) return retryAfterSeconds; + const standardized = parseIntHeader(getHeader(headers, 'ratelimit-reset')); + if (standardized !== undefined) { + return Math.max(1, standardized); + } + const legacyEpochSeconds = parseIntHeader(getHeader(headers, 'x-ratelimit-reset')); + if (legacyEpochSeconds === undefined) return undefined; + const nowSeconds = Math.floor(nowMs / 1000); + return Math.max(1, legacyEpochSeconds - nowSeconds); } function parseRetryAfterSeconds(value: string | undefined, nowMs: number): number | undefined { - if (!value) return undefined; - const trimmed = value.trim(); - if (!trimmed) return undefined; - - const asNumber = Number(trimmed); - if (Number.isFinite(asNumber) && asNumber >= 0) { - if (asNumber > 31_536_000) { - const nowSeconds = Math.floor(nowMs / 1000); - return Math.max(1, Math.ceil(asNumber - nowSeconds)); + if (!value) return undefined; + const trimmed = value.trim(); + if (!trimmed) return undefined; + + const asNumber = Number(trimmed); + if (Number.isFinite(asNumber) && asNumber >= 0) { + if (asNumber > 31_536_000) { + const nowSeconds = Math.floor(nowMs / 1000); + return Math.max(1, Math.ceil(asNumber - nowSeconds)); + } + return Math.max(1, Math.ceil(asNumber)); } - return Math.max(1, Math.ceil(asNumber)); - } - const asDateMs = Date.parse(trimmed); - if (!Number.isFinite(asDateMs)) return undefined; - return Math.max(1, Math.ceil((asDateMs - nowMs) / 1000)); + const asDateMs = Date.parse(trimmed); + if (!Number.isFinite(asDateMs)) return undefined; + return Math.max(1, Math.ceil((asDateMs - nowMs) / 1000)); } function parseIntHeader(value: string | undefined): number | undefined { - if (!value) return undefined; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : undefined; + if (!value) return undefined; + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : undefined; } function getHeader(headers: HeaderSource, key: string): string | undefined { - if (!headers) return undefined; - if (headers instanceof Headers) { - const value = headers.get(key); - return value === null ? undefined : value; - } - const normalizedKey = key.toLowerCase(); - const direct = headers[normalizedKey] ?? headers[key]; - if (typeof direct === "string" && direct.trim()) return direct.trim(); - const match = Object.entries(headers).find( - ([entryKey, entryValue]) => - entryKey.toLowerCase() === normalizedKey && - typeof entryValue === "string" && - entryValue.trim(), - ); - return typeof match?.[1] === "string" ? match[1].trim() : undefined; + if (!headers) return undefined; + if (headers instanceof Headers) { + const value = headers.get(key); + return value === null ? undefined : value; + } + const normalizedKey = key.toLowerCase(); + const direct = headers[normalizedKey] ?? headers[key]; + if (typeof direct === 'string' && direct.trim()) return direct.trim(); + const match = Object.entries(headers).find( + ([entryKey, entryValue]) => + entryKey.toLowerCase() === normalizedKey && + typeof entryValue === 'string' && + entryValue.trim() + ); + return typeof match?.[1] === 'string' ? match[1].trim() : undefined; } async function fetchJsonViaCurl( - deps: Pick, - url: string, - args: RequestArgs, -) { - const headers = ["-H", "Accept: application/json"]; - const curlArgs = [ - "--silent", - "--show-error", - "--location", - "--max-time", - String(REQUEST_TIMEOUT_SECONDS), - "--write-out", - CURL_WRITE_OUT_FORMAT, - "-X", - args.method, - ...headers, - url, - ]; - if (args.body !== undefined || args.method === "POST") { - curlArgs.push("-H", "Content-Type: application/json"); - curlArgs.push("--data-binary", JSON.stringify(args.body ?? {})); - } - - const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); - if (result.status !== 0) { - throw new Error(result.stderr || "curl failed"); - } - const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); - if (status < 200 || status >= 300) { - throwHttpStatusError(status, body, responseHeaders, deps.now); - } - return JSON.parse(body || "null") as unknown; -} - -async function fetchJsonFormViaCurl( - deps: Pick< - HttpClientDeps, - | "spawnSyncImpl" - | "mkdtempImpl" - | "mkdirImpl" - | "writeFileImpl" - | "rmImpl" - | "tmpdirPath" - | "now" - >, - url: string, - args: FormRequestArgs, + deps: Pick, + url: string, + args: RequestArgs ) { - const headers = ["-H", "Accept: application/json"]; - - const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "dt-skill-upload-")); - try { - const formArgs: string[] = []; - for (const [key, value] of args.form.entries()) { - if (value instanceof Blob) { - const filename = typeof (value as File).name === "string" ? (value as File).name : "file"; - const filePath = join(tempDir, filename); - const bytes = new Uint8Array(await value.arrayBuffer()); - await deps.mkdirImpl(dirname(filePath), { recursive: true }); - await deps.writeFileImpl(filePath, bytes); - formArgs.push("-F", `${key}=@${filePath};filename=${filename}`); - } else { - formArgs.push("-F", `${key}=${value}`); - } - } - + const headers = ['-H', 'Accept: application/json']; const curlArgs = [ - "--silent", - "--show-error", - "--location", - "--max-time", - String(UPLOAD_TIMEOUT_SECONDS), - "--write-out", - CURL_WRITE_OUT_FORMAT, - "-X", - args.method, - ...headers, - ...formArgs, - url, + '--silent', + '--show-error', + '--location', + '--max-time', + String(REQUEST_TIMEOUT_SECONDS), + '--write-out', + CURL_WRITE_OUT_FORMAT, + '-X', + args.method, + ...headers, + url, ]; + if (args.body !== undefined || args.method === 'POST') { + curlArgs.push('-H', 'Content-Type: application/json'); + curlArgs.push('--data-binary', JSON.stringify(args.body ?? {})); + } - const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); + const result = deps.spawnSyncImpl('curl', curlArgs, { encoding: 'utf8' }); if (result.status !== 0) { - throw new Error(result.stderr || "curl failed"); + throw new Error(result.stderr || 'curl failed'); } - const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); + const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ''); if (status < 200 || status >= 300) { - throwHttpStatusError(status, body, responseHeaders, deps.now); + throwHttpStatusError(status, body, responseHeaders, deps.now); } - return JSON.parse(body || "null") as unknown; - } finally { - await deps.rmImpl(tempDir, { recursive: true, force: true }); - } + return JSON.parse(body || 'null') as unknown; } -async function fetchTextViaCurl( - deps: Pick, - url: string, +async function fetchJsonFormViaCurl( + deps: Pick< + HttpClientDeps, + | 'spawnSyncImpl' + | 'mkdtempImpl' + | 'mkdirImpl' + | 'writeFileImpl' + | 'rmImpl' + | 'tmpdirPath' + | 'now' + >, + url: string, + args: FormRequestArgs ) { - const headers = ["-H", "Accept: text/plain"]; - const curlArgs = [ - "--silent", - "--show-error", - "--location", - "--max-time", - String(REQUEST_TIMEOUT_SECONDS), - "--write-out", - CURL_WRITE_OUT_FORMAT, - "-X", - "GET", - ...headers, - url, - ]; - const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); - if (result.status !== 0) { - throw new Error(result.stderr || "curl failed"); - } - const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); - if (status < 200 || status >= 300) { - throwHttpStatusError(status, body, responseHeaders, deps.now); - } - return body; + const headers = ['-H', 'Accept: application/json']; + + const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, 'dt-skill-upload-')); + try { + const formArgs: string[] = []; + for (const [key, value] of args.form.entries()) { + if (value instanceof Blob) { + const filename = + typeof (value as File).name === 'string' ? (value as File).name : 'file'; + const filePath = join(tempDir, filename); + const bytes = new Uint8Array(await value.arrayBuffer()); + await deps.mkdirImpl(dirname(filePath), { recursive: true }); + await deps.writeFileImpl(filePath, bytes); + formArgs.push('-F', `${key}=@${filePath};filename=${filename}`); + } else { + formArgs.push('-F', `${key}=${value}`); + } + } + + const curlArgs = [ + '--silent', + '--show-error', + '--location', + '--max-time', + String(UPLOAD_TIMEOUT_SECONDS), + '--write-out', + CURL_WRITE_OUT_FORMAT, + '-X', + args.method, + ...headers, + ...formArgs, + url, + ]; + + const result = deps.spawnSyncImpl('curl', curlArgs, { encoding: 'utf8' }); + if (result.status !== 0) { + throw new Error(result.stderr || 'curl failed'); + } + const { + body, + status, + headers: responseHeaders, + } = parseCurlBodyAndMeta(result.stdout ?? ''); + if (status < 200 || status >= 300) { + throwHttpStatusError(status, body, responseHeaders, deps.now); + } + return JSON.parse(body || 'null') as unknown; + } finally { + await deps.rmImpl(tempDir, { recursive: true, force: true }); + } } -async function fetchBinaryViaCurl( - deps: Pick< - HttpClientDeps, - "spawnSyncImpl" | "mkdtempImpl" | "readFileImpl" | "rmImpl" | "tmpdirPath" | "now" - >, - url: string, -) { - const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, "dt-skill-download-")); - const filePath = join(tempDir, "payload.bin"); - try { - const headers: string[] = []; - +async function fetchTextViaCurl(deps: Pick, url: string) { + const headers = ['-H', 'Accept: text/plain']; const curlArgs = [ - "--silent", - "--show-error", - "--location", - "--max-time", - String(REQUEST_TIMEOUT_SECONDS), - ...headers, - "-o", - filePath, - "--write-out", - CURL_WRITE_OUT_FORMAT, - url, + '--silent', + '--show-error', + '--location', + '--max-time', + String(REQUEST_TIMEOUT_SECONDS), + '--write-out', + CURL_WRITE_OUT_FORMAT, + '-X', + 'GET', + ...headers, + url, ]; - const result = deps.spawnSyncImpl("curl", curlArgs, { encoding: "utf8" }); + const result = deps.spawnSyncImpl('curl', curlArgs, { encoding: 'utf8' }); if (result.status !== 0) { - throw new Error(result.stderr || "curl failed"); + throw new Error(result.stderr || 'curl failed'); } - const { status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ""); + const { body, status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ''); if (status < 200 || status >= 300) { - const body = await readFileSafe(deps.readFileImpl, filePath); - throwHttpStatusError( - status, - body ? new TextDecoder().decode(body) : "", - responseHeaders, - deps.now, - ); + throwHttpStatusError(status, body, responseHeaders, deps.now); + } + return body; +} + +async function fetchBinaryViaCurl( + deps: Pick< + HttpClientDeps, + 'spawnSyncImpl' | 'mkdtempImpl' | 'readFileImpl' | 'rmImpl' | 'tmpdirPath' | 'now' + >, + url: string +) { + const tempDir = await deps.mkdtempImpl(join(deps.tmpdirPath, 'dt-skill-download-')); + const filePath = join(tempDir, 'payload.bin'); + try { + const headers: string[] = []; + + const curlArgs = [ + '--silent', + '--show-error', + '--location', + '--max-time', + String(REQUEST_TIMEOUT_SECONDS), + ...headers, + '-o', + filePath, + '--write-out', + CURL_WRITE_OUT_FORMAT, + url, + ]; + const result = deps.spawnSyncImpl('curl', curlArgs, { encoding: 'utf8' }); + if (result.status !== 0) { + throw new Error(result.stderr || 'curl failed'); + } + const { status, headers: responseHeaders } = parseCurlBodyAndMeta(result.stdout ?? ''); + if (status < 200 || status >= 300) { + const body = await readFileSafe(deps.readFileImpl, filePath); + throwHttpStatusError( + status, + body ? new TextDecoder().decode(body) : '', + responseHeaders, + deps.now + ); + } + const bytes = await readFileSafe(deps.readFileImpl, filePath); + return bytes ? new Uint8Array(bytes) : new Uint8Array(); + } finally { + await deps.rmImpl(tempDir, { recursive: true, force: true }); } - const bytes = await readFileSafe(deps.readFileImpl, filePath); - return bytes ? new Uint8Array(bytes) : new Uint8Array(); - } finally { - await deps.rmImpl(tempDir, { recursive: true, force: true }); - } } function parseCurlBodyAndMeta(output: string): { - body: string; - status: number; - headers: Record; + body: string; + status: number; + headers: Record; } { - const marker = `\n${CURL_META_MARKER}\n`; - const markerIndex = output.lastIndexOf(marker); - if (markerIndex === -1) { - const splitAt = output.lastIndexOf("\n"); - if (splitAt === -1) { - const statusOnly = Number(output.trim()); - if (!Number.isFinite(statusOnly)) throw new Error("curl response missing status"); - return { body: "", status: statusOnly, headers: {} }; + const marker = `\n${CURL_META_MARKER}\n`; + const markerIndex = output.lastIndexOf(marker); + if (markerIndex === -1) { + const splitAt = output.lastIndexOf('\n'); + if (splitAt === -1) { + const statusOnly = Number(output.trim()); + if (!Number.isFinite(statusOnly)) throw new Error('curl response missing status'); + return { body: '', status: statusOnly, headers: {} }; + } + const body = output.slice(0, splitAt); + const status = Number(output.slice(splitAt + 1).trim()); + if (!Number.isFinite(status)) throw new Error('curl response missing status'); + return { body, status, headers: {} }; } - const body = output.slice(0, splitAt); - const status = Number(output.slice(splitAt + 1).trim()); - if (!Number.isFinite(status)) throw new Error("curl response missing status"); - return { body, status, headers: {} }; - } - - const body = output.slice(0, markerIndex); - const meta = output.slice(markerIndex + marker.length).replace(/\r/g, ""); - const lines = meta.split("\n"); - const status = Number((lines[0] ?? "").trim()); - if (!Number.isFinite(status)) throw new Error("curl response missing status"); - - const [ - xRateLimitLimit, - xRateLimitRemaining, - xRateLimitReset, - rateLimitLimit, - rateLimitRemaining, - rateLimitReset, - retryAfter, - ] = lines.slice(1); - - const headers: Record = {}; - setHeaderIfPresent(headers, "x-ratelimit-limit", xRateLimitLimit); - setHeaderIfPresent(headers, "x-ratelimit-remaining", xRateLimitRemaining); - setHeaderIfPresent(headers, "x-ratelimit-reset", xRateLimitReset); - setHeaderIfPresent(headers, "ratelimit-limit", rateLimitLimit); - setHeaderIfPresent(headers, "ratelimit-remaining", rateLimitRemaining); - setHeaderIfPresent(headers, "ratelimit-reset", rateLimitReset); - setHeaderIfPresent(headers, "retry-after", retryAfter); - - return { body, status, headers }; + + const body = output.slice(0, markerIndex); + const meta = output.slice(markerIndex + marker.length).replace(/\r/g, ''); + const lines = meta.split('\n'); + const status = Number((lines[0] ?? '').trim()); + if (!Number.isFinite(status)) throw new Error('curl response missing status'); + + const [ + xRateLimitLimit, + xRateLimitRemaining, + xRateLimitReset, + rateLimitLimit, + rateLimitRemaining, + rateLimitReset, + retryAfter, + ] = lines.slice(1); + + const headers: Record = {}; + setHeaderIfPresent(headers, 'x-ratelimit-limit', xRateLimitLimit); + setHeaderIfPresent(headers, 'x-ratelimit-remaining', xRateLimitRemaining); + setHeaderIfPresent(headers, 'x-ratelimit-reset', xRateLimitReset); + setHeaderIfPresent(headers, 'ratelimit-limit', rateLimitLimit); + setHeaderIfPresent(headers, 'ratelimit-remaining', rateLimitRemaining); + setHeaderIfPresent(headers, 'ratelimit-reset', rateLimitReset); + setHeaderIfPresent(headers, 'retry-after', retryAfter); + + return { body, status, headers }; } function setHeaderIfPresent( - headers: Record, - key: string, - value: string | undefined, + headers: Record, + key: string, + value: string | undefined ) { - if (typeof value !== "string") return; - const trimmed = value.trim(); - if (!trimmed) return; - headers[key] = trimmed; + if (typeof value !== 'string') return; + const trimmed = value.trim(); + if (!trimmed) return; + headers[key] = trimmed; } async function readFileSafe(readFileImpl: typeof readFile, path: string) { - try { - return await readFileImpl(path); - } catch { - return null; - } + try { + return await readFileImpl(path); + } catch { + return null; + } } diff --git a/dt-skill/src/schema/ark.ts b/dt-skill/src/schema/ark.ts index 335096b5..8720409c 100644 --- a/dt-skill/src/schema/ark.ts +++ b/dt-skill/src/schema/ark.ts @@ -1,29 +1,29 @@ -import { ArkErrors } from "arktype"; +import { ArkErrors } from 'arktype'; export type ArkValidator = (data: unknown) => T | ArkErrors; export function parseArk(schema: ArkValidator, data: unknown, label: string) { - const result = schema(data); - if (result instanceof ArkErrors) { - throw new Error(`${label}: ${formatArkErrors(result)}`); - } - return result; + const result = schema(data); + if (result instanceof ArkErrors) { + throw new Error(`${label}: ${formatArkErrors(result)}`); + } + return result; } export function formatArkErrors(errors: ArkErrors) { - const parts: string[] = []; - for (const error of errors) { - if (parts.length >= 3) break; - const path = Array.isArray(error.path) ? error.path.join(".") : ""; - const location = path ? `${path}: ` : ""; - const description = - typeof (error as { description?: unknown }).description === "string" - ? ((error as { description: string }).description as string) - : "invalid value"; - parts.push(`${location}${description}`); - } - if (errors.count > parts.length) { - parts.push(`+${errors.count - parts.length} more`); - } - return parts.join("; "); + const parts: string[] = []; + for (const error of errors) { + if (parts.length >= 3) break; + const path = Array.isArray(error.path) ? error.path.join('.') : ''; + const location = path ? `${path}: ` : ''; + const description = + typeof (error as { description?: unknown }).description === 'string' + ? ((error as { description: string }).description as string) + : 'invalid value'; + parts.push(`${location}${description}`); + } + if (errors.count > parts.length) { + parts.push(`+${errors.count - parts.length} more`); + } + return parts.join('; '); } diff --git a/dt-skill/src/schema/clawScanNote.ts b/dt-skill/src/schema/clawScanNote.ts index 7d296b59..73916395 100644 --- a/dt-skill/src/schema/clawScanNote.ts +++ b/dt-skill/src/schema/clawScanNote.ts @@ -1,10 +1,10 @@ export const MAX_CLAWSCAN_NOTE_CHARS = 4000; export function normalizeClawScanNote(value: string | null | undefined) { - const trimmed = value?.trim() ?? ""; - if (!trimmed) return undefined; - if (trimmed.length > MAX_CLAWSCAN_NOTE_CHARS) { - throw new Error(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); - } - return trimmed; + const trimmed = value?.trim() ?? ''; + if (!trimmed) return undefined; + if (trimmed.length > MAX_CLAWSCAN_NOTE_CHARS) { + throw new Error(`ClawScan note must be at most ${MAX_CLAWSCAN_NOTE_CHARS} characters.`); + } + return trimmed; } diff --git a/dt-skill/src/schema/index.ts b/dt-skill/src/schema/index.ts index 6b992d52..df15dd51 100644 --- a/dt-skill/src/schema/index.ts +++ b/dt-skill/src/schema/index.ts @@ -1,7 +1,7 @@ -export type { ArkValidator } from "./ark.js"; -export { parseArk } from "./ark.js"; -export * from "./clawScanNote.js"; -export { PLATFORM_SKILL_LICENSE, PLATFORM_SKILL_LICENSE_SUMMARY } from "./license.js"; -export { ApiRoutes, LegacyApiRoutes } from "./routes.js"; -export * from "./schemas.js"; -export * from "./textFiles.js"; +export type { ArkValidator } from './ark.js'; +export { parseArk } from './ark.js'; +export * from './clawScanNote.js'; +export { PLATFORM_SKILL_LICENSE, PLATFORM_SKILL_LICENSE_SUMMARY } from './license.js'; +export { ApiRoutes, LegacyApiRoutes } from './routes.js'; +export * from './schemas.js'; +export * from './textFiles.js'; diff --git a/dt-skill/src/schema/license.ts b/dt-skill/src/schema/license.ts index 4036c036..5a0c8fa8 100644 --- a/dt-skill/src/schema/license.ts +++ b/dt-skill/src/schema/license.ts @@ -1,5 +1,5 @@ -export const PLATFORM_SKILL_LICENSE = "MIT-0" as const; -export const PLATFORM_SKILL_LICENSE_NAME = "MIT No Attribution" as const; +export const PLATFORM_SKILL_LICENSE = 'MIT-0' as const; +export const PLATFORM_SKILL_LICENSE_NAME = 'MIT No Attribution' as const; export const PLATFORM_SKILL_LICENSE_SUMMARY = - "Free to use, modify, and redistribute. No attribution required." as const; -export const PLATFORM_SKILL_LICENSE_URL = "https://spdx.org/licenses/MIT-0.html" as const; + 'Free to use, modify, and redistribute. No attribution required.' as const; +export const PLATFORM_SKILL_LICENSE_URL = 'https://spdx.org/licenses/MIT-0.html' as const; diff --git a/dt-skill/src/schema/routes.ts b/dt-skill/src/schema/routes.ts index 31544c8f..9091e88b 100644 --- a/dt-skill/src/schema/routes.ts +++ b/dt-skill/src/schema/routes.ts @@ -1,23 +1,23 @@ export const LegacyApiRoutes = { - download: "/api/download", - search: "/api/search", - skill: "/api/skill", - skillResolve: "/api/skill/resolve", - cliUploadUrl: "/api/cli/upload-url", - cliPublish: "/api/cli/publish", - cliTelemetrySync: "/api/cli/telemetry/sync", - cliSkillDelete: "/api/cli/skill/delete", - cliSkillUndelete: "/api/cli/skill/undelete", + download: '/api/download', + search: '/api/search', + skill: '/api/skill', + skillResolve: '/api/skill/resolve', + cliUploadUrl: '/api/cli/upload-url', + cliPublish: '/api/cli/publish', + cliTelemetrySync: '/api/cli/telemetry/sync', + cliSkillDelete: '/api/cli/skill/delete', + cliSkillUndelete: '/api/cli/skill/undelete', } as const; export const ApiRoutes = { - search: "/api/v1/search", - resolve: "/api/v1/resolve", - download: "/api/v1/download", - skills: "/api/v1/skills", - stars: "/api/v1/stars", - transfers: "/api/v1/transfers", - publishers: "/api/v1/publishers", - souls: "/api/v1/souls", - users: "/api/v1/users", + search: '/api/v1/search', + resolve: '/api/v1/resolve', + download: '/api/v1/download', + skills: '/api/v1/skills', + stars: '/api/v1/stars', + transfers: '/api/v1/transfers', + publishers: '/api/v1/publishers', + souls: '/api/v1/souls', + users: '/api/v1/users', } as const; diff --git a/dt-skill/src/schema/schemas.test.ts b/dt-skill/src/schema/schemas.test.ts index 468d680b..dfc82a3e 100644 --- a/dt-skill/src/schema/schemas.test.ts +++ b/dt-skill/src/schema/schemas.test.ts @@ -1,53 +1,53 @@ /* @vitest-environment node */ -import { describe, expect, it } from "vitest"; -import { parseArk } from "./ark"; -import { ApiV1SearchResponseSchema, ClawdisSkillMetadataSchema } from "./schemas"; +import { describe, expect, it } from 'vitest'; +import { parseArk } from './ark'; +import { ApiV1SearchResponseSchema, ClawdisSkillMetadataSchema } from './schemas'; -describe("dt-skill skill metadata schema", () => { - it("preserves optional env var declarations", () => { - const parsed = parseArk( - ClawdisSkillMetadataSchema, - { - envVars: [ - { name: "TODOIST_API_KEY", required: true, description: "API token" }, - { name: "TODOIST_PROJECT_ID", required: false, description: "Default project" }, - ], - }, - "Skill metadata", - ); +describe('dt-skill skill metadata schema', () => { + it('preserves optional env var declarations', () => { + const parsed = parseArk( + ClawdisSkillMetadataSchema, + { + envVars: [ + { name: 'TODOIST_API_KEY', required: true, description: 'API token' }, + { name: 'TODOIST_PROJECT_ID', required: false, description: 'Default project' }, + ], + }, + 'Skill metadata' + ); - expect(parsed.envVars?.[1]).toEqual({ - name: "TODOIST_PROJECT_ID", - required: false, - description: "Default project", + expect(parsed.envVars?.[1]).toEqual({ + name: 'TODOIST_PROJECT_ID', + required: false, + description: 'Default project', + }); }); - }); - it("parses v1 search owner metadata", () => { - const parsed = parseArk( - ApiV1SearchResponseSchema, - { - results: [ - { - slug: "demo", - displayName: "Demo", - summary: null, - version: "1.0.0", - score: 1, - ownerHandle: "openclaw", - owner: { - handle: "openclaw", - displayName: "OpenClaw", - image: null, + it('parses v1 search owner metadata', () => { + const parsed = parseArk( + ApiV1SearchResponseSchema, + { + results: [ + { + slug: 'demo', + displayName: 'Demo', + summary: null, + version: '1.0.0', + score: 1, + ownerHandle: 'openclaw', + owner: { + handle: 'openclaw', + displayName: 'OpenClaw', + image: null, + }, + }, + ], }, - }, - ], - }, - "Search", - ); + 'Search' + ); - expect(parsed.results[0]?.ownerHandle).toBe("openclaw"); - expect(parsed.results[0]?.owner?.displayName).toBe("OpenClaw"); - }); + expect(parsed.results[0]?.ownerHandle).toBe('openclaw'); + expect(parsed.results[0]?.owner?.displayName).toBe('OpenClaw'); + }); }); diff --git a/dt-skill/src/schema/schemas.ts b/dt-skill/src/schema/schemas.ts index 6513c591..6fc325e3 100644 --- a/dt-skill/src/schema/schemas.ts +++ b/dt-skill/src/schema/schemas.ts @@ -1,583 +1,582 @@ -import { type inferred, type } from "arktype"; +import { type inferred, type } from 'arktype'; export const GlobalConfigSchema = type({ - registry: "string", + registry: 'string', }); -export type GlobalConfig = (typeof GlobalConfigSchema)[inferred]; +export type GlobalConfig = typeof GlobalConfigSchema[inferred]; export const WellKnownConfigSchema = type({ - apiBase: "string", - minCliVersion: "string?", + apiBase: 'string', + minCliVersion: 'string?', }).or({ - registry: "string", - minCliVersion: "string?", + registry: 'string', + minCliVersion: 'string?', }); -export type WellKnownConfig = (typeof WellKnownConfigSchema)[inferred]; +export type WellKnownConfig = typeof WellKnownConfigSchema[inferred]; export const LockfileSchema = type({ - version: "1", - skills: { - "[string]": { - version: "string|null", - installedAt: "number", - pinned: "boolean?", - pinReason: "string?", + version: '1', + skills: { + '[string]': { + version: 'string|null', + installedAt: 'number', + pinned: 'boolean?', + pinReason: 'string?', + }, }, - }, }); -export type Lockfile = (typeof LockfileSchema)[inferred]; +export type Lockfile = typeof LockfileSchema[inferred]; export const ApiSearchResponseSchema = type({ - results: type({ - slug: "string?", - displayName: "string?", - version: "string|null?", - score: "number", - }).array(), + results: type({ + slug: 'string?', + displayName: 'string?', + version: 'string|null?', + score: 'number', + }).array(), }); export const ApiSkillMetaResponseSchema = type({ - latestVersion: type({ - version: "string", - }).optional(), - skill: "unknown|null?", + latestVersion: type({ + version: 'string', + }).optional(), + skill: 'unknown|null?', }); export const ApiCliUploadUrlResponseSchema = type({ - uploadUrl: "string", + uploadUrl: 'string', }); export const ApiUploadFileResponseSchema = type({ - storageId: "string", + storageId: 'string', }); export const CliPublishFileSchema = type({ - path: "string", - size: "number", - storageId: "string", - sha256: "string", - contentType: "string?", + path: 'string', + size: 'number', + storageId: 'string', + sha256: 'string', + contentType: 'string?', }); -export type CliPublishFile = (typeof CliPublishFileSchema)[inferred]; +export type CliPublishFile = typeof CliPublishFileSchema[inferred]; export const PublishSourceSchema = type({ - kind: '"github"', - url: "string", - repo: "string", - ref: "string", - commit: "string", - path: "string", - importedAt: "number", + kind: '"github"', + url: 'string', + repo: 'string', + ref: 'string', + commit: 'string', + path: 'string', + importedAt: 'number', }); export const CliPublishRequestSchema = type({ - slug: "string", - displayName: "string", - ownerHandle: "string?", - migrateOwner: "boolean?", - version: "string", - changelog: "string", - clawScanNote: "string?", - acceptLicenseTerms: "boolean?", - tags: "string[]?", - source: PublishSourceSchema.optional(), - forkOf: type({ - slug: "string", - version: "string?", - }).optional(), - files: CliPublishFileSchema.array(), -}); -export type CliPublishRequest = (typeof CliPublishRequestSchema)[inferred]; + slug: 'string', + displayName: 'string', + ownerHandle: 'string?', + migrateOwner: 'boolean?', + version: 'string', + changelog: 'string', + clawScanNote: 'string?', + acceptLicenseTerms: 'boolean?', + tags: 'string[]?', + source: PublishSourceSchema.optional(), + forkOf: type({ + slug: 'string', + version: 'string?', + }).optional(), + files: CliPublishFileSchema.array(), +}); +export type CliPublishRequest = typeof CliPublishRequestSchema[inferred]; export const ApiCliPublishResponseSchema = type({ - ok: "true", - skillId: "string", - versionId: "string", + ok: 'true', + skillId: 'string', + versionId: 'string', }); export const CliSkillDeleteRequestSchema = type({ - slug: "string", - reason: "string?", + slug: 'string', + reason: 'string?', }); -export type CliSkillDeleteRequest = (typeof CliSkillDeleteRequestSchema)[inferred]; +export type CliSkillDeleteRequest = typeof CliSkillDeleteRequestSchema[inferred]; export const ApiCliSkillDeleteResponseSchema = type({ - ok: "true", - slugReservedUntil: "number?", + ok: 'true', + slugReservedUntil: 'number?', }); export const ApiSkillResolveResponseSchema = type({ - match: type({ version: "string" }).or("null"), - latestVersion: type({ version: "string" }).or("null"), + match: type({ version: 'string' }).or('null'), + latestVersion: type({ version: 'string' }).or('null'), }); export const CliTelemetrySyncRequestSchema = type({ - roots: type({ - rootId: "string", - label: "string", - skills: type({ - slug: "string", - version: "string|null?", + roots: type({ + rootId: 'string', + label: 'string', + skills: type({ + slug: 'string', + version: 'string|null?', + }).array(), }).array(), - }).array(), }); -export type CliTelemetrySyncRequest = (typeof CliTelemetrySyncRequestSchema)[inferred]; +export type CliTelemetrySyncRequest = typeof CliTelemetrySyncRequestSchema[inferred]; export const ApiCliTelemetrySyncResponseSchema = type({ - ok: "true", + ok: 'true', }); export const ApiV1WhoamiResponseSchema = type({ - user: { - handle: "string|null", - displayName: "string|null?", - image: "string|null?", - role: '"admin"|"moderator"|"user"|null?', - }, + user: { + handle: 'string|null', + displayName: 'string|null?', + image: 'string|null?', + role: '"admin"|"moderator"|"user"|null?', + }, }); export const ApiV1UserSearchResponseSchema = type({ - items: type({ - userId: "string", - handle: "string|null", - displayName: "string|null?", - name: "string|null?", - role: '"admin"|"moderator"|"user"|null?', - }).array(), - total: "number", + items: type({ + userId: 'string', + handle: 'string|null', + displayName: 'string|null?', + name: 'string|null?', + role: '"admin"|"moderator"|"user"|null?', + }).array(), + total: 'number', }); export const ApiV1PublisherCreateResponseSchema = type({ - ok: "true", - publisherId: "string", - handle: "string", - created: "true", - trusted: "false", + ok: 'true', + publisherId: 'string', + handle: 'string', + created: 'true', + trusted: 'false', }); -export type ApiV1PublisherCreateResponse = (typeof ApiV1PublisherCreateResponseSchema)[inferred]; +export type ApiV1PublisherCreateResponse = typeof ApiV1PublisherCreateResponseSchema[inferred]; export const ApiV1SearchResponseSchema = type({ - results: type({ - slug: "string?", - displayName: "string?", - summary: "string|null?", - version: "string|null?", - score: "number", - updatedAt: "number?", - ownerHandle: "string|null?", - owner: type({ - handle: "string|null?", - displayName: "string|null?", - image: "string|null?", - }) - .or("null") - .optional(), - }).array(), + results: type({ + slug: 'string?', + displayName: 'string?', + summary: 'string|null?', + version: 'string|null?', + score: 'number', + updatedAt: 'number?', + ownerHandle: 'string|null?', + owner: type({ + handle: 'string|null?', + displayName: 'string|null?', + image: 'string|null?', + }) + .or('null') + .optional(), + }).array(), }); -export type ApiV1SearchResponse = (typeof ApiV1SearchResponseSchema)[inferred]; +export type ApiV1SearchResponse = typeof ApiV1SearchResponseSchema[inferred]; export const ApiV1SkillListResponseSchema = type({ - items: type({ - slug: "string", - displayName: "string", - summary: "string|null?", - tags: "unknown", - stats: "unknown", - createdAt: "number", - updatedAt: "number", - latestVersion: type({ - version: "string", - createdAt: "number", - changelog: "string", - license: '"MIT-0"|null?', - }).optional(), - }).array(), - nextCursor: "string|null", + items: type({ + slug: 'string', + displayName: 'string', + summary: 'string|null?', + tags: 'unknown', + stats: 'unknown', + createdAt: 'number', + updatedAt: 'number', + latestVersion: type({ + version: 'string', + createdAt: 'number', + changelog: 'string', + license: '"MIT-0"|null?', + }).optional(), + }).array(), + nextCursor: 'string|null', }); -export type ApiV1SkillListResponse = (typeof ApiV1SkillListResponseSchema)[inferred]; +export type ApiV1SkillListResponse = typeof ApiV1SkillListResponseSchema[inferred]; export const ApiV1SkillResponseSchema = type({ - skill: type({ - slug: "string", - displayName: "string", - summary: "string|null?", - tags: "unknown", - stats: "unknown", - createdAt: "number", - updatedAt: "number", - isPackage: "boolean?", - parentSlug: "string|null?", - children: "unknown[]?", - }).or("null"), - latestVersion: type({ - version: "string", - createdAt: "number", - changelog: "string", - license: '"MIT-0"|null?', - }).or("null"), - owner: type({ - handle: "string|null", - displayName: "string|null?", - image: "string|null?", - }).or("null"), - moderation: type({ - isSuspicious: "boolean", - isMalwareBlocked: "boolean", - verdict: '"clean"|"suspicious"|"malicious"?', - reasonCodes: "string[]?", - updatedAt: "number|null?", - engineVersion: "string|null?", - summary: "string|null?", - }) - .or("null") - .optional(), -}); -export type ApiV1SkillResponse = (typeof ApiV1SkillResponseSchema)[inferred]; + skill: type({ + slug: 'string', + displayName: 'string', + summary: 'string|null?', + tags: 'unknown', + stats: 'unknown', + createdAt: 'number', + updatedAt: 'number', + isPackage: 'boolean?', + parentSlug: 'string|null?', + children: 'unknown[]?', + }).or('null'), + latestVersion: type({ + version: 'string', + createdAt: 'number', + changelog: 'string', + license: '"MIT-0"|null?', + }).or('null'), + owner: type({ + handle: 'string|null', + displayName: 'string|null?', + image: 'string|null?', + }).or('null'), + moderation: type({ + isSuspicious: 'boolean', + isMalwareBlocked: 'boolean', + verdict: '"clean"|"suspicious"|"malicious"?', + reasonCodes: 'string[]?', + updatedAt: 'number|null?', + engineVersion: 'string|null?', + summary: 'string|null?', + }) + .or('null') + .optional(), +}); +export type ApiV1SkillResponse = typeof ApiV1SkillResponseSchema[inferred]; export const ApiV1SkillModerationResponseSchema = type({ - moderation: type({ - isSuspicious: "boolean", - isMalwareBlocked: "boolean", - verdict: '"clean"|"suspicious"|"malicious"', - reasonCodes: "string[]", - updatedAt: "number|null?", - engineVersion: "string|null?", - summary: "string|null?", - legacyReason: "string|null?", - evidence: type({ - code: "string", - severity: '"info"|"warn"|"critical"', - file: "string", - line: "number", - message: "string", - evidence: "string", - }).array(), - }).or("null"), + moderation: type({ + isSuspicious: 'boolean', + isMalwareBlocked: 'boolean', + verdict: '"clean"|"suspicious"|"malicious"', + reasonCodes: 'string[]', + updatedAt: 'number|null?', + engineVersion: 'string|null?', + summary: 'string|null?', + legacyReason: 'string|null?', + evidence: type({ + code: 'string', + severity: '"info"|"warn"|"critical"', + file: 'string', + line: 'number', + message: 'string', + evidence: 'string', + }).array(), + }).or('null'), }); export const SkillReportStatusSchema = type('"open"|"confirmed"|"dismissed"'); -export type SkillReportStatus = (typeof SkillReportStatusSchema)[inferred]; +export type SkillReportStatus = typeof SkillReportStatusSchema[inferred]; export const SkillReportFinalActionSchema = type('"none"|"hide"'); -export type SkillReportFinalAction = (typeof SkillReportFinalActionSchema)[inferred]; +export type SkillReportFinalAction = typeof SkillReportFinalActionSchema[inferred]; export const SkillReportListStatusSchema = SkillReportStatusSchema.or('"all"'); -export type SkillReportListStatus = (typeof SkillReportListStatusSchema)[inferred]; +export type SkillReportListStatus = typeof SkillReportListStatusSchema[inferred]; export const SkillAppealStatusSchema = type('"open"|"accepted"|"rejected"'); -export type SkillAppealStatus = (typeof SkillAppealStatusSchema)[inferred]; +export type SkillAppealStatus = typeof SkillAppealStatusSchema[inferred]; export const SkillAppealFinalActionSchema = type('"none"|"restore"'); -export type SkillAppealFinalAction = (typeof SkillAppealFinalActionSchema)[inferred]; +export type SkillAppealFinalAction = typeof SkillAppealFinalActionSchema[inferred]; export const SkillAppealListStatusSchema = SkillAppealStatusSchema.or('"all"'); -export type SkillAppealListStatus = (typeof SkillAppealListStatusSchema)[inferred]; +export type SkillAppealListStatus = typeof SkillAppealListStatusSchema[inferred]; export const SkillAppealRequestSchema = type({ - version: "string?", - message: "string", + version: 'string?', + message: 'string', }); -export type SkillAppealRequest = (typeof SkillAppealRequestSchema)[inferred]; +export type SkillAppealRequest = typeof SkillAppealRequestSchema[inferred]; export const ApiV1SkillReportResponseSchema = type({ - ok: "true", - reported: "boolean", - alreadyReported: "boolean", - reportId: "string", - skillId: "string", - reportCount: "number", + ok: 'true', + reported: 'boolean', + alreadyReported: 'boolean', + reportId: 'string', + skillId: 'string', + reportCount: 'number', }); -export type ApiV1SkillReportResponse = (typeof ApiV1SkillReportResponseSchema)[inferred]; +export type ApiV1SkillReportResponse = typeof ApiV1SkillReportResponseSchema[inferred]; export const ApiV1SkillAppealResponseSchema = type({ - ok: "true", - submitted: "boolean", - alreadyOpen: "boolean", - appealId: "string", - skillId: "string", - status: SkillAppealStatusSchema, + ok: 'true', + submitted: 'boolean', + alreadyOpen: 'boolean', + appealId: 'string', + skillId: 'string', + status: SkillAppealStatusSchema, }); -export type ApiV1SkillAppealResponse = (typeof ApiV1SkillAppealResponseSchema)[inferred]; +export type ApiV1SkillAppealResponse = typeof ApiV1SkillAppealResponseSchema[inferred]; export const SkillReportTriageRequestSchema = type({ - status: SkillReportStatusSchema, - note: "string?", - finalAction: SkillReportFinalActionSchema.optional(), + status: SkillReportStatusSchema, + note: 'string?', + finalAction: SkillReportFinalActionSchema.optional(), }); -export type SkillReportTriageRequest = (typeof SkillReportTriageRequestSchema)[inferred]; +export type SkillReportTriageRequest = typeof SkillReportTriageRequestSchema[inferred]; export const SkillAppealResolveRequestSchema = type({ - status: SkillAppealStatusSchema, - note: "string?", - finalAction: SkillAppealFinalActionSchema.optional(), + status: SkillAppealStatusSchema, + note: 'string?', + finalAction: SkillAppealFinalActionSchema.optional(), }); -export type SkillAppealResolveRequest = (typeof SkillAppealResolveRequestSchema)[inferred]; +export type SkillAppealResolveRequest = typeof SkillAppealResolveRequestSchema[inferred]; export const ApiV1SkillReportListResponseSchema = type({ - items: type({ - reportId: "string", - skillId: "string", - skillVersionId: "string|null?", - slug: "string", - displayName: "string", - version: "string|null?", - reason: "string|null?", - status: SkillReportStatusSchema, - createdAt: "number", - reporter: type({ - userId: "string", - handle: "string|null?", - displayName: "string|null?", - }), - triagedAt: "number|null?", - triagedBy: "string|null?", - triageNote: "string|null?", - actionTaken: SkillReportFinalActionSchema.or("null").optional(), - }).array(), - nextCursor: "string|null", - done: "boolean", -}); -export type ApiV1SkillReportListResponse = (typeof ApiV1SkillReportListResponseSchema)[inferred]; + items: type({ + reportId: 'string', + skillId: 'string', + skillVersionId: 'string|null?', + slug: 'string', + displayName: 'string', + version: 'string|null?', + reason: 'string|null?', + status: SkillReportStatusSchema, + createdAt: 'number', + reporter: type({ + userId: 'string', + handle: 'string|null?', + displayName: 'string|null?', + }), + triagedAt: 'number|null?', + triagedBy: 'string|null?', + triageNote: 'string|null?', + actionTaken: SkillReportFinalActionSchema.or('null').optional(), + }).array(), + nextCursor: 'string|null', + done: 'boolean', +}); +export type ApiV1SkillReportListResponse = typeof ApiV1SkillReportListResponseSchema[inferred]; export const ApiV1SkillReportTriageResponseSchema = type({ - ok: "true", - reportId: "string", - skillId: "string", - status: SkillReportStatusSchema, - reportCount: "number", - actionTaken: SkillReportFinalActionSchema.optional(), + ok: 'true', + reportId: 'string', + skillId: 'string', + status: SkillReportStatusSchema, + reportCount: 'number', + actionTaken: SkillReportFinalActionSchema.optional(), }); -export type ApiV1SkillReportTriageResponse = - (typeof ApiV1SkillReportTriageResponseSchema)[inferred]; +export type ApiV1SkillReportTriageResponse = typeof ApiV1SkillReportTriageResponseSchema[inferred]; export const ApiV1SkillAppealListResponseSchema = type({ - items: type({ - appealId: "string", - skillId: "string", - skillVersionId: "string|null?", - slug: "string", - displayName: "string", - version: "string|null?", - message: "string", - status: SkillAppealStatusSchema, - createdAt: "number", - submitter: type({ - userId: "string", - handle: "string|null?", - displayName: "string|null?", - }), - resolvedAt: "number|null?", - resolvedBy: "string|null?", - resolutionNote: "string|null?", - actionTaken: SkillAppealFinalActionSchema.or("null").optional(), - }).array(), - nextCursor: "string|null", - done: "boolean", -}); -export type ApiV1SkillAppealListResponse = (typeof ApiV1SkillAppealListResponseSchema)[inferred]; + items: type({ + appealId: 'string', + skillId: 'string', + skillVersionId: 'string|null?', + slug: 'string', + displayName: 'string', + version: 'string|null?', + message: 'string', + status: SkillAppealStatusSchema, + createdAt: 'number', + submitter: type({ + userId: 'string', + handle: 'string|null?', + displayName: 'string|null?', + }), + resolvedAt: 'number|null?', + resolvedBy: 'string|null?', + resolutionNote: 'string|null?', + actionTaken: SkillAppealFinalActionSchema.or('null').optional(), + }).array(), + nextCursor: 'string|null', + done: 'boolean', +}); +export type ApiV1SkillAppealListResponse = typeof ApiV1SkillAppealListResponseSchema[inferred]; export const ApiV1SkillAppealResolveResponseSchema = type({ - ok: "true", - appealId: "string", - skillId: "string", - status: SkillAppealStatusSchema, - actionTaken: SkillAppealFinalActionSchema.optional(), + ok: 'true', + appealId: 'string', + skillId: 'string', + status: SkillAppealStatusSchema, + actionTaken: SkillAppealFinalActionSchema.optional(), }); export type ApiV1SkillAppealResolveResponse = - (typeof ApiV1SkillAppealResolveResponseSchema)[inferred]; + typeof ApiV1SkillAppealResolveResponseSchema[inferred]; export const ApiV1SkillRescanResponseSchema = type({ - ok: "true", - slug: "string", - version: "string", - skillId: "string", - skillVersionId: "string", - jobId: "string", - alreadyQueued: "boolean", + ok: 'true', + slug: 'string', + version: 'string', + skillId: 'string', + skillVersionId: 'string', + jobId: 'string', + alreadyQueued: 'boolean', }); -export type ApiV1SkillRescanResponse = (typeof ApiV1SkillRescanResponseSchema)[inferred]; +export type ApiV1SkillRescanResponse = typeof ApiV1SkillRescanResponseSchema[inferred]; export const ApiV1SkillVersionListResponseSchema = type({ - items: type({ - version: "string", - createdAt: "number", - changelog: "string", - changelogSource: '"auto"|"user"|null?', - }).array(), - nextCursor: "string|null", + items: type({ + version: 'string', + createdAt: 'number', + changelog: 'string', + changelogSource: '"auto"|"user"|null?', + }).array(), + nextCursor: 'string|null', }); export const ApiV1SkillVersionResponseSchema = type({ - version: type({ - version: "string", - createdAt: "number", - changelog: "string", - changelogSource: '"auto"|"user"|null?', - license: '"MIT-0"|null?', - files: "unknown?", - }).or("null"), - skill: type({ - slug: "string", - displayName: "string", - }).or("null"), -}); -export type ApiV1SkillVersionResponse = (typeof ApiV1SkillVersionResponseSchema)[inferred]; + version: type({ + version: 'string', + createdAt: 'number', + changelog: 'string', + changelogSource: '"auto"|"user"|null?', + license: '"MIT-0"|null?', + files: 'unknown?', + }).or('null'), + skill: type({ + slug: 'string', + displayName: 'string', + }).or('null'), +}); +export type ApiV1SkillVersionResponse = typeof ApiV1SkillVersionResponseSchema[inferred]; export const ApiV1SkillResolveResponseSchema = type({ - match: type({ version: "string" }).or("null"), - latestVersion: type({ version: "string" }).or("null"), + match: type({ version: 'string' }).or('null'), + latestVersion: type({ version: 'string' }).or('null'), }); -export type ApiV1SkillResolveResponse = (typeof ApiV1SkillResolveResponseSchema)[inferred]; +export type ApiV1SkillResolveResponse = typeof ApiV1SkillResolveResponseSchema[inferred]; export const ApiV1PublishResponseSchema = type({ - ok: "true", - skillId: "string", - versionId: "string", + ok: 'true', + skillId: 'string', + versionId: 'string', }); export const ApiV1DeleteResponseSchema = type({ - ok: "true", - slugReservedUntil: "number?", + ok: 'true', + slugReservedUntil: 'number?', }); export const ApiV1SkillRenameResponseSchema = type({ - ok: "true", - slug: "string", - previousSlug: "string", + ok: 'true', + slug: 'string', + previousSlug: 'string', }); export const ApiV1SkillMergeResponseSchema = type({ - ok: "true", - sourceSlug: "string", - targetSlug: "string", + ok: 'true', + sourceSlug: 'string', + targetSlug: 'string', }); export const ApiV1TransferRequestResponseSchema = type({ - ok: "true", - transferId: "string?", - toUserHandle: "string?", - toPublisherHandle: "string?", - skillSlug: "string?", - expiresAt: "number?", - transferred: "boolean?", + ok: 'true', + transferId: 'string?', + toUserHandle: 'string?', + toPublisherHandle: 'string?', + skillSlug: 'string?', + expiresAt: 'number?', + transferred: 'boolean?', }); export const ApiV1TransferDecisionResponseSchema = type({ - ok: "true", - skillSlug: "string?", + ok: 'true', + skillSlug: 'string?', }); export const ApiV1TransferListResponseSchema = type({ - transfers: type({ - _id: "string", - skill: type({ - _id: "string", - slug: "string", - displayName: "string", - }), - fromUser: type({ - _id: "string", - handle: "string|null", - displayName: "string|null", - }).optional(), - toUser: type({ - _id: "string", - handle: "string|null", - displayName: "string|null", - }).optional(), - message: "string?", - requestedAt: "number", - expiresAt: "number", - }).array(), + transfers: type({ + _id: 'string', + skill: type({ + _id: 'string', + slug: 'string', + displayName: 'string', + }), + fromUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + toUser: type({ + _id: 'string', + handle: 'string|null', + displayName: 'string|null', + }).optional(), + message: 'string?', + requestedAt: 'number', + expiresAt: 'number', + }).array(), }); export const ApiV1BanUserResponseSchema = type({ - ok: "true", - alreadyBanned: "boolean", - deletedSkills: "number", + ok: 'true', + alreadyBanned: 'boolean', + deletedSkills: 'number', }); export const ApiV1UnbanUserResponseSchema = type({ - ok: "true", - alreadyUnbanned: "boolean", - restoredSkills: "number?", + ok: 'true', + alreadyUnbanned: 'boolean', + restoredSkills: 'number?', }); export const ApiV1ReclassifyBanResponseSchema = type({ - ok: "true", - dryRun: "boolean", - userId: "string", - handle: "string|null", - previousReason: "string|null", - nextReason: "string", - changed: "boolean", + ok: 'true', + dryRun: 'boolean', + userId: 'string', + handle: 'string|null', + previousReason: 'string|null', + nextReason: 'string', + changed: 'boolean', }); export const ApiV1RemediateAutobansResponseSchema = type({ - ok: "true", - dryRun: "boolean", - scanned: "number", - wouldUnban: "number", - unbanned: "number", - skipped: "number", - restoredSkills: "number", - restoredPackages: "number", - items: "unknown[]", - "nextCursor?": "string|null", - "done?": "boolean", + ok: 'true', + dryRun: 'boolean', + scanned: 'number', + wouldUnban: 'number', + unbanned: 'number', + skipped: 'number', + restoredSkills: 'number', + restoredPackages: 'number', + items: 'unknown[]', + 'nextCursor?': 'string|null', + 'done?': 'boolean', }); export const ApiV1SetRoleResponseSchema = type({ - ok: "true", - role: '"admin"|"moderator"|"user"', + ok: 'true', + role: '"admin"|"moderator"|"user"', }); export const ApiV1StarResponseSchema = type({ - ok: "true", - starred: "boolean", - alreadyStarred: "boolean", + ok: 'true', + starred: 'boolean', + alreadyStarred: 'boolean', }); export const ApiV1UnstarResponseSchema = type({ - ok: "true", - unstarred: "boolean", - alreadyUnstarred: "boolean", + ok: 'true', + unstarred: 'boolean', + alreadyUnstarred: 'boolean', }); export const SkillInstallSpecSchema = type({ - id: "string?", - kind: '"brew"|"node"|"go"|"uv"', - label: "string?", - bins: "string[]?", - formula: "string?", - tap: "string?", - package: "string?", - module: "string?", + id: 'string?', + kind: '"brew"|"node"|"go"|"uv"', + label: 'string?', + bins: 'string[]?', + formula: 'string?', + tap: 'string?', + package: 'string?', + module: 'string?', }); -export type SkillInstallSpec = (typeof SkillInstallSpecSchema)[inferred]; +export type SkillInstallSpec = typeof SkillInstallSpecSchema[inferred]; export const ClawdisRequiresSchema = type({ - bins: "string[]?", - anyBins: "string[]?", - env: "string[]?", - config: "string[]?", + bins: 'string[]?', + anyBins: 'string[]?', + env: 'string[]?', + config: 'string[]?', }); -export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred]; +export type ClawdisRequires = typeof ClawdisRequiresSchema[inferred]; export const EnvVarDeclarationSchema = type({ - name: "string", - required: "boolean?", - description: "string?", + name: 'string', + required: 'boolean?', + description: 'string?', }); -export type EnvVarDeclaration = (typeof EnvVarDeclarationSchema)[inferred]; +export type EnvVarDeclaration = typeof EnvVarDeclarationSchema[inferred]; export const ClawdisSkillMetadataSchema = type({ - always: "boolean?", - skillKey: "string?", - primaryEnv: "string?", - emoji: "string?", - homepage: "string?", - os: "string[]?", - requires: ClawdisRequiresSchema.optional(), - install: SkillInstallSpecSchema.array().optional(), - envVars: EnvVarDeclarationSchema.array().optional(), -}); -export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred]; + always: 'boolean?', + skillKey: 'string?', + primaryEnv: 'string?', + emoji: 'string?', + homepage: 'string?', + os: 'string[]?', + requires: ClawdisRequiresSchema.optional(), + install: SkillInstallSpecSchema.array().optional(), + envVars: EnvVarDeclarationSchema.array().optional(), +}); +export type ClawdisSkillMetadata = typeof ClawdisSkillMetadataSchema[inferred]; diff --git a/dt-skill/src/schema/skillFingerprintContract.test.ts b/dt-skill/src/schema/skillFingerprintContract.test.ts index 9a334fe1..3361daec 100644 --- a/dt-skill/src/schema/skillFingerprintContract.test.ts +++ b/dt-skill/src/schema/skillFingerprintContract.test.ts @@ -1,36 +1,40 @@ /* @vitest-environment node */ -import { readFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; -import { buildSkillFingerprint, fingerprintFromGoldenCase, sha256Hex } from "./skillFingerprintContract.js"; +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { + buildSkillFingerprint, + fingerprintFromGoldenCase, + sha256Hex, +} from './skillFingerprintContract.js'; -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..'); const goldenVectors = JSON.parse( - readFileSync(resolve(repoRoot, "contracts/skill-fingerprint/golden-vectors.v1.json"), "utf8"), + readFileSync(resolve(repoRoot, 'contracts/skill-fingerprint/golden-vectors.v1.json'), 'utf8') ); -describe("skill fingerprint contract adapter", () => { - it("matches shared golden vectors", () => { - for (const testCase of goldenVectors.cases) { - const fingerprint = fingerprintFromGoldenCase(testCase); - if (testCase.fingerprint) { - expect(fingerprint, testCase.name).toBe(testCase.fingerprint); - } - expect(fingerprintFromGoldenCase(testCase), testCase.name).toBe(fingerprint); - } - }); +describe('skill fingerprint contract adapter', () => { + it('matches shared golden vectors', () => { + for (const testCase of goldenVectors.cases) { + const fingerprint = fingerprintFromGoldenCase(testCase); + if (testCase.fingerprint) { + expect(fingerprint, testCase.name).toBe(testCase.fingerprint); + } + expect(fingerprintFromGoldenCase(testCase), testCase.name).toBe(fingerprint); + } + }); - it("sorts paths before hashing", () => { - const fingerprint = buildSkillFingerprint([ - { path: "b.txt", sha256: sha256Hex("b") }, - { path: "a.txt", sha256: sha256Hex("a") }, - ]); - const expected = buildSkillFingerprint([ - { path: "a.txt", sha256: sha256Hex("a") }, - { path: "b.txt", sha256: sha256Hex("b") }, - ]); - expect(fingerprint).toBe(expected); - }); + it('sorts paths before hashing', () => { + const fingerprint = buildSkillFingerprint([ + { path: 'b.txt', sha256: sha256Hex('b') }, + { path: 'a.txt', sha256: sha256Hex('a') }, + ]); + const expected = buildSkillFingerprint([ + { path: 'a.txt', sha256: sha256Hex('a') }, + { path: 'b.txt', sha256: sha256Hex('b') }, + ]); + expect(fingerprint).toBe(expected); + }); }); diff --git a/dt-skill/src/schema/skillFingerprintContract.ts b/dt-skill/src/schema/skillFingerprintContract.ts index dc4dfb7c..c82231dc 100644 --- a/dt-skill/src/schema/skillFingerprintContract.ts +++ b/dt-skill/src/schema/skillFingerprintContract.ts @@ -1,57 +1,55 @@ -import { createRequire } from "node:module"; -import { dirname, resolve, sep } from "node:path"; -import { fileURLToPath } from "node:url"; +import { createRequire } from 'node:module'; +import { dirname, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const currentDir = dirname(fileURLToPath(import.meta.url)); const contractPath = currentDir.endsWith(`${sep}src${sep}schema`) - ? resolve(currentDir, "../../dist/contracts/skill-fingerprint/index.cjs") - : resolve(currentDir, "../contracts/skill-fingerprint/index.cjs"); + ? resolve(currentDir, '../../dist/contracts/skill-fingerprint/index.cjs') + : resolve(currentDir, '../contracts/skill-fingerprint/index.cjs'); const contract = require(contractPath); export const TEXT_FILE_EXTENSIONS = contract.TEXT_FILE_EXTENSIONS as readonly string[]; export const TEXT_FILE_EXTENSION_SET = contract.TEXT_FILE_EXTENSION_SET as ReadonlySet; export const FINGERPRINT_IGNORE_FILENAMES = - contract.FINGERPRINT_IGNORE_FILENAMES as readonly string[]; + contract.FINGERPRINT_IGNORE_FILENAMES as readonly string[]; export const TEXT_SAMPLE_BYTES = contract.TEXT_SAMPLE_BYTES as number; export const normalizeFilePath = contract.normalizeFilePath as (filePath: string) => string; export const getFileExtension = contract.getFileExtension as (filePath: string) => string; export const hasDotPathSegment = contract.hasDotPathSegment as (filePath: string) => boolean; export const isLikelyTextBytes = contract.isLikelyTextBytes as ( - bytes: Uint8Array | Buffer, + bytes: Uint8Array | Buffer ) => boolean; export const shouldIncludeFingerprintFile = contract.shouldIncludeFingerprintFile as (options: { - filePath: string; - isBinary?: boolean; - bytes?: Uint8Array | Buffer; - ignoreMatcher?: { ignores(path: string): boolean } | null; + filePath: string; + isBinary?: boolean; + bytes?: Uint8Array | Buffer; + ignoreMatcher?: { ignores(path: string): boolean } | null; }) => boolean; export const sha256Hex = contract.sha256Hex as (bytes: Uint8Array | Buffer) => string; export const buildSkillFingerprint = contract.buildSkillFingerprint as ( - files: Array<{ path: string; sha256: string }>, + files: Array<{ path: string; sha256: string }> ) => string; export const buildSkillFingerprintFromStoredFiles = - contract.buildSkillFingerprintFromStoredFiles as ( - files: Array<{ - file_path?: string; - path?: string; - content?: string; - is_binary?: number; - isBinary?: boolean; - encoding?: string; - }>, - options?: { ignoreMatcher?: { ignores(path: string): boolean } | null }, - ) => string; -export const fingerprintFromGoldenCase = contract.fingerprintFromGoldenCase as ( - testCase: { + contract.buildSkillFingerprintFromStoredFiles as ( + files: Array<{ + file_path?: string; + path?: string; + content?: string; + is_binary?: number; + isBinary?: boolean; + encoding?: string; + }>, + options?: { ignoreMatcher?: { ignores(path: string): boolean } | null } + ) => string; +export const fingerprintFromGoldenCase = contract.fingerprintFromGoldenCase as (testCase: { ignoreFiles?: Array<{ path: string; content: string }>; files: Array<{ - path: string; - content: string; - encoding?: string; - isBinary?: boolean; + path: string; + content: string; + encoding?: string; + isBinary?: boolean; }>; - }, -) => string; +}) => string; diff --git a/dt-skill/src/schema/textFiles.test.ts b/dt-skill/src/schema/textFiles.test.ts index e6bb1f6b..c7e23603 100644 --- a/dt-skill/src/schema/textFiles.test.ts +++ b/dt-skill/src/schema/textFiles.test.ts @@ -1,31 +1,31 @@ /* @vitest-environment node */ -import { describe, expect, it } from "vitest"; -import * as schema from "."; -import { isTextContentType, TEXT_FILE_EXTENSION_SET } from "./textFiles"; +import { describe, expect, it } from 'vitest'; +import * as schema from '.'; +import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles'; -describe("dt-skill schema textFiles", () => { - it("exports text-file extension set", () => { - expect(TEXT_FILE_EXTENSION_SET.has("md")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("r")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("ps1")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("psm1")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("psd1")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("tsv")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("conf")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("properties")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("dat")).toBe(true); - expect(TEXT_FILE_EXTENSION_SET.has("exe")).toBe(false); - }); +describe('dt-skill schema textFiles', () => { + it('exports text-file extension set', () => { + expect(TEXT_FILE_EXTENSION_SET.has('md')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('r')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('ps1')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('psm1')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('psd1')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('tsv')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('conf')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('properties')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('dat')).toBe(true); + expect(TEXT_FILE_EXTENSION_SET.has('exe')).toBe(false); + }); - it("detects text content types with parameters", () => { - expect(isTextContentType("text/plain; charset=utf-8")).toBe(true); - expect(isTextContentType("application/json; charset=utf-8")).toBe(true); - expect(isTextContentType("application/octet-stream")).toBe(false); - }); + it('detects text content types with parameters', () => { + expect(isTextContentType('text/plain; charset=utf-8')).toBe(true); + expect(isTextContentType('application/json; charset=utf-8')).toBe(true); + expect(isTextContentType('application/octet-stream')).toBe(false); + }); - it("re-exports helpers from index", () => { - expect(typeof schema.isTextContentType).toBe("function"); - expect(schema.isTextContentType("application/markdown")).toBe(true); - }); + it('re-exports helpers from index', () => { + expect(typeof schema.isTextContentType).toBe('function'); + expect(schema.isTextContentType('application/markdown')).toBe(true); + }); }); diff --git a/dt-skill/src/schema/textFiles.ts b/dt-skill/src/schema/textFiles.ts index 66f859ba..09ff9d55 100644 --- a/dt-skill/src/schema/textFiles.ts +++ b/dt-skill/src/schema/textFiles.ts @@ -1,18 +1,15 @@ -import { - TEXT_FILE_EXTENSIONS, - TEXT_FILE_EXTENSION_SET, -} from "./skillFingerprintContract.js"; +import { TEXT_FILE_EXTENSIONS, TEXT_FILE_EXTENSION_SET } from './skillFingerprintContract.js'; const RAW_TEXT_CONTENT_TYPES = [ - "application/json", - "application/xml", - "application/yaml", - "application/x-yaml", - "application/toml", - "application/javascript", - "application/typescript", - "application/markdown", - "image/svg+xml", + 'application/json', + 'application/xml', + 'application/yaml', + 'application/x-yaml', + 'application/toml', + 'application/javascript', + 'application/typescript', + 'application/markdown', + 'image/svg+xml', ] as const; export { TEXT_FILE_EXTENSIONS, TEXT_FILE_EXTENSION_SET }; @@ -21,9 +18,9 @@ export const TEXT_CONTENT_TYPES = RAW_TEXT_CONTENT_TYPES; export const TEXT_CONTENT_TYPE_SET = new Set(TEXT_CONTENT_TYPES); export function isTextContentType(contentType: string) { - if (!contentType) return false; - const normalized = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? ""; - if (!normalized) return false; - if (normalized.startsWith("text/")) return true; - return TEXT_CONTENT_TYPE_SET.has(normalized); + if (!contentType) return false; + const normalized = contentType.split(';', 1)[0]?.trim().toLowerCase() ?? ''; + if (!normalized) return false; + if (normalized.startsWith('text/')) return true; + return TEXT_CONTENT_TYPE_SET.has(normalized); } diff --git a/dt-skill/src/skills.test.ts b/dt-skill/src/skills.test.ts index cdae8534..622774a2 100644 --- a/dt-skill/src/skills.test.ts +++ b/dt-skill/src/skills.test.ts @@ -1,282 +1,286 @@ /* @vitest-environment node */ -import { mkdir, mkdtemp, readFile, stat, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { strToU8, zipSync } from "fflate"; -import { describe, expect, it } from "vitest"; -import type { SkillOrigin } from "./skills"; +import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { strToU8, zipSync } from 'fflate'; +import { describe, expect, it } from 'vitest'; +import type { SkillOrigin } from './skills'; import { - buildSkillFingerprint, - extractZipToDir, - hashSkillFiles, - hashSkillZip, - listManualSkills, - listTextFiles, - readLockfile, - readSkillOrigin, - sha256Hex, - writeLockfile, - writeSkillOrigin, -} from "./skills"; + buildSkillFingerprint, + extractZipToDir, + hashSkillFiles, + hashSkillZip, + listManualSkills, + listTextFiles, + readLockfile, + readSkillOrigin, + sha256Hex, + writeLockfile, + writeSkillOrigin, +} from './skills'; -describe("skills", () => { - it("extracts zip into directory and skips traversal", async () => { - const parent = await mkdtemp(join(tmpdir(), "dt-skill-zip-")); - const dir = join(parent, "dir"); - await mkdir(dir); - const evilName = `evil-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; - const zip = zipSync({ - "SKILL.md": strToU8("hello"), - [`../${evilName}`]: strToU8("nope"), - }); - await extractZipToDir(new Uint8Array(zip), dir); +describe('skills', () => { + it('extracts zip into directory and skips traversal', async () => { + const parent = await mkdtemp(join(tmpdir(), 'dt-skill-zip-')); + const dir = join(parent, 'dir'); + await mkdir(dir); + const evilName = `evil-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; + const zip = zipSync({ + 'SKILL.md': strToU8('hello'), + [`../${evilName}`]: strToU8('nope'), + }); + await extractZipToDir(new Uint8Array(zip), dir); - expect((await readFile(join(dir, "SKILL.md"), "utf8")).trim()).toBe("hello"); - await expect(stat(join(parent, evilName))).rejects.toBeTruthy(); - }); + expect((await readFile(join(dir, 'SKILL.md'), 'utf8')).trim()).toBe('hello'); + await expect(stat(join(parent, evilName))).rejects.toBeTruthy(); + }); - it("writes and reads lockfile", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-work-")); - await writeLockfile(workdir, { - version: 1, - skills: { - demo: { - version: "1.0.0", - installedAt: 1, - pinned: true, - pinReason: "awaiting moderation review", - }, - }, + it('writes and reads lockfile', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-work-')); + await writeLockfile(workdir, { + version: 1, + skills: { + demo: { + version: '1.0.0', + installedAt: 1, + pinned: true, + pinReason: 'awaiting moderation review', + }, + }, + }); + const read = await readLockfile(workdir); + expect(read.skills.demo?.version).toBe('1.0.0'); + expect(read.skills.demo?.pinned).toBe(true); + expect(read.skills.demo?.pinReason).toBe('awaiting moderation review'); }); - const read = await readLockfile(workdir); - expect(read.skills.demo?.version).toBe("1.0.0"); - expect(read.skills.demo?.pinned).toBe(true); - expect(read.skills.demo?.pinReason).toBe("awaiting moderation review"); - }); - it("returns empty lockfile on invalid json", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-work-bad-")); - await mkdir(join(workdir, ".dt-skill"), { recursive: true }); - await writeFile(join(workdir, ".dt-skill", "lock.json"), "{", "utf8"); - const read = await readLockfile(workdir); - expect(read).toEqual({ version: 1, skills: {} }); - }); + it('returns empty lockfile on invalid json', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-work-bad-')); + await mkdir(join(workdir, '.dt-skill'), { recursive: true }); + await writeFile(join(workdir, '.dt-skill', 'lock.json'), '{', 'utf8'); + const read = await readLockfile(workdir); + expect(read).toEqual({ version: 1, skills: {} }); + }); - it("returns empty lockfile on schema mismatch", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-work-schema-")); - await mkdir(join(workdir, ".dt-skill"), { recursive: true }); - await writeFile( - join(workdir, ".dt-skill", "lock.json"), - JSON.stringify({ version: 1, skills: "nope" }), - "utf8", - ); - const read = await readLockfile(workdir); - expect(read).toEqual({ version: 1, skills: {} }); - }); + it('returns empty lockfile on schema mismatch', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-work-schema-')); + await mkdir(join(workdir, '.dt-skill'), { recursive: true }); + await writeFile( + join(workdir, '.dt-skill', 'lock.json'), + JSON.stringify({ version: 1, skills: 'nope' }), + 'utf8' + ); + const read = await readLockfile(workdir); + expect(read).toEqual({ version: 1, skills: {} }); + }); - it("skips dotfiles and node_modules when listing text files", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-files-")); - await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); - await writeFile(join(workdir, ".secret.txt"), "no", "utf8"); - await mkdir(join(workdir, ".dt-skill"), { recursive: true }); - await writeFile(join(workdir, ".dt-skill", "origin.json"), "{}", "utf8"); - await mkdir(join(workdir, "node_modules"), { recursive: true }); - await writeFile(join(workdir, "node_modules", "a.txt"), "no", "utf8"); - const files = await listTextFiles(workdir); - expect(files.map((file) => file.relPath)).toEqual(["SKILL.md"]); - }); + it('skips dotfiles and node_modules when listing text files', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-files-')); + await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8'); + await writeFile(join(workdir, '.secret.txt'), 'no', 'utf8'); + await mkdir(join(workdir, '.dt-skill'), { recursive: true }); + await writeFile(join(workdir, '.dt-skill', 'origin.json'), '{}', 'utf8'); + await mkdir(join(workdir, 'node_modules'), { recursive: true }); + await writeFile(join(workdir, 'node_modules', 'a.txt'), 'no', 'utf8'); + const files = await listTextFiles(workdir); + expect(files.map((file) => file.relPath)).toEqual(['SKILL.md']); + }); - it("respects .gitignore and .dt-skillignore", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-ignore-")); - await writeFile(join(workdir, ".gitignore"), "ignored.md\n", "utf8"); - await writeFile(join(workdir, ".dt-skillignore"), "private.md\n", "utf8"); - await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); - await writeFile(join(workdir, "ignored.md"), "no", "utf8"); - await writeFile(join(workdir, "private.md"), "no", "utf8"); - await writeFile(join(workdir, "public.json"), "{}", "utf8"); + it('respects .gitignore and .dt-skillignore', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-ignore-')); + await writeFile(join(workdir, '.gitignore'), 'ignored.md\n', 'utf8'); + await writeFile(join(workdir, '.dt-skillignore'), 'private.md\n', 'utf8'); + await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8'); + await writeFile(join(workdir, 'ignored.md'), 'no', 'utf8'); + await writeFile(join(workdir, 'private.md'), 'no', 'utf8'); + await writeFile(join(workdir, 'public.json'), '{}', 'utf8'); - const files = await listTextFiles(workdir); - const paths = files.map((file) => file.relPath).sort(); - expect(paths).toEqual(["SKILL.md", "public.json"]); - expect(files.find((file) => file.relPath === "SKILL.md")?.contentType).toMatch(/^text\//); - expect(files.find((file) => file.relPath === "public.json")?.contentType).toBe( - "application/json", - ); - }); + const files = await listTextFiles(workdir); + const paths = files.map((file) => file.relPath).sort(); + expect(paths).toEqual(['SKILL.md', 'public.json']); + expect(files.find((file) => file.relPath === 'SKILL.md')?.contentType).toMatch(/^text\//); + expect(files.find((file) => file.relPath === 'public.json')?.contentType).toBe( + 'application/json' + ); + }); - it("falls back to text/plain for unknown text extensions", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-env-")); - await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); - await writeFile(join(workdir, "config.env"), "TOKEN=demo", "utf8"); - const files = await listTextFiles(workdir); - expect(files.find((file) => file.relPath === "config.env")?.contentType).toBe("text/plain"); - }); + it('falls back to text/plain for unknown text extensions', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-env-')); + await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8'); + await writeFile(join(workdir, 'config.env'), 'TOKEN=demo', 'utf8'); + const files = await listTextFiles(workdir); + expect(files.find((file) => file.relPath === 'config.env')?.contentType).toBe('text/plain'); + }); - it("includes tsv and extensionless text files while skipping extensionless binaries", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-extensionless-")); - await writeFile(join(workdir, "SKILL.md"), "hi", "utf8"); - await writeFile(join(workdir, "config.tsv"), "name\tvalue\napi\tok\n", "utf8"); - await writeFile(join(workdir, ".npmrc"), "//registry.npmjs.org/:_authToken=secret\n", "utf8"); - await mkdir(join(workdir, "bin"), { recursive: true }); - await writeFile( - join(workdir, "bin", "openclaw-kraken"), - "#!/usr/bin/env sh\necho ok\n", - "utf8", - ); - const largeBinary = new Uint8Array(1024 * 1024); - largeBinary[0] = 0; - largeBinary[largeBinary.length - 1] = 255; - await writeFile(join(workdir, "bin", "binary"), largeBinary); + it('includes tsv and extensionless text files while skipping extensionless binaries', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-extensionless-')); + await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8'); + await writeFile(join(workdir, 'config.tsv'), 'name\tvalue\napi\tok\n', 'utf8'); + await writeFile( + join(workdir, '.npmrc'), + '//registry.npmjs.org/:_authToken=secret\n', + 'utf8' + ); + await mkdir(join(workdir, 'bin'), { recursive: true }); + await writeFile( + join(workdir, 'bin', 'openclaw-kraken'), + '#!/usr/bin/env sh\necho ok\n', + 'utf8' + ); + const largeBinary = new Uint8Array(1024 * 1024); + largeBinary[0] = 0; + largeBinary[largeBinary.length - 1] = 255; + await writeFile(join(workdir, 'bin', 'binary'), largeBinary); - const files = await listTextFiles(workdir); - const paths = files.map((file) => file.relPath).sort(); - expect(paths).toEqual(["SKILL.md", "bin/openclaw-kraken", "config.tsv"]); - expect(files.find((file) => file.relPath === "bin/openclaw-kraken")?.contentType).toBe( - "text/plain", - ); - }); + const files = await listTextFiles(workdir); + const paths = files.map((file) => file.relPath).sort(); + expect(paths).toEqual(['SKILL.md', 'bin/openclaw-kraken', 'config.tsv']); + expect(files.find((file) => file.relPath === 'bin/openclaw-kraken')?.contentType).toBe( + 'text/plain' + ); + }); - it("hashes skill files deterministically", async () => { - const { fingerprint } = hashSkillFiles([ - { relPath: "b.txt", bytes: strToU8("b") }, - { relPath: "a.txt", bytes: strToU8("a") }, - ]); - const expected = buildSkillFingerprint([ - { path: "a.txt", sha256: sha256Hex(strToU8("a")) }, - { path: "b.txt", sha256: sha256Hex(strToU8("b")) }, - ]); - expect(fingerprint).toBe(expected); - }); + it('hashes skill files deterministically', async () => { + const { fingerprint } = hashSkillFiles([ + { relPath: 'b.txt', bytes: strToU8('b') }, + { relPath: 'a.txt', bytes: strToU8('a') }, + ]); + const expected = buildSkillFingerprint([ + { path: 'a.txt', sha256: sha256Hex(strToU8('a')) }, + { path: 'b.txt', sha256: sha256Hex(strToU8('b')) }, + ]); + expect(fingerprint).toBe(expected); + }); - it("hashes text files inside a downloaded zip deterministically", () => { - const zip = zipSync({ - "SKILL.md": strToU8("hello"), - "notes.md": strToU8("world"), - ".npmrc": strToU8("//registry.npmjs.org/:_authToken=secret\n"), - "config/endpoints.tsv": strToU8("name\turl\napi\thttps://example.com\n"), - "bin/tool": strToU8("#!/usr/bin/env sh\necho ok\n"), - "image.png": strToU8("nope"), + it('hashes text files inside a downloaded zip deterministically', () => { + const zip = zipSync({ + 'SKILL.md': strToU8('hello'), + 'notes.md': strToU8('world'), + '.npmrc': strToU8('//registry.npmjs.org/:_authToken=secret\n'), + 'config/endpoints.tsv': strToU8('name\turl\napi\thttps://example.com\n'), + 'bin/tool': strToU8('#!/usr/bin/env sh\necho ok\n'), + 'image.png': strToU8('nope'), + }); + const { fingerprint } = hashSkillZip(new Uint8Array(zip)); + const expected = buildSkillFingerprint([ + { path: 'SKILL.md', sha256: sha256Hex(strToU8('hello')) }, + { path: 'bin/tool', sha256: sha256Hex(strToU8('#!/usr/bin/env sh\necho ok\n')) }, + { + path: 'config/endpoints.tsv', + sha256: sha256Hex(strToU8('name\turl\napi\thttps://example.com\n')), + }, + { path: 'notes.md', sha256: sha256Hex(strToU8('world')) }, + ]); + expect(fingerprint).toBe(expected); }); - const { fingerprint } = hashSkillZip(new Uint8Array(zip)); - const expected = buildSkillFingerprint([ - { path: "SKILL.md", sha256: sha256Hex(strToU8("hello")) }, - { path: "bin/tool", sha256: sha256Hex(strToU8("#!/usr/bin/env sh\necho ok\n")) }, - { - path: "config/endpoints.tsv", - sha256: sha256Hex(strToU8("name\turl\napi\thttps://example.com\n")), - }, - { path: "notes.md", sha256: sha256Hex(strToU8("world")) }, - ]); - expect(fingerprint).toBe(expected); - }); - it("ignores unsafe or non-text entries when hashing zips", () => { - const zip = zipSync({ - "SKILL.md": strToU8("hello"), - "folder/": strToU8(""), - "../evil.txt": strToU8("nope"), - "bad\\path.txt": strToU8("nope"), - "image.png": strToU8("nope"), + it('ignores unsafe or non-text entries when hashing zips', () => { + const zip = zipSync({ + 'SKILL.md': strToU8('hello'), + 'folder/': strToU8(''), + '../evil.txt': strToU8('nope'), + 'bad\\path.txt': strToU8('nope'), + 'image.png': strToU8('nope'), + }); + const { files } = hashSkillZip(new Uint8Array(zip)); + expect(files).toEqual([{ path: 'SKILL.md', sha256: sha256Hex(strToU8('hello')), size: 5 }]); }); - const { files } = hashSkillZip(new Uint8Array(zip)); - expect(files).toEqual([{ path: "SKILL.md", sha256: sha256Hex(strToU8("hello")), size: 5 }]); - }); - it("builds fingerprints from valid entries only", () => { - const fingerprint = buildSkillFingerprint([ - { path: "", sha256: "" }, - { path: "valid.txt", sha256: sha256Hex(strToU8("ok")) }, - ]); - const expected = buildSkillFingerprint([ - { path: "valid.txt", sha256: sha256Hex(strToU8("ok")) }, - ]); - expect(fingerprint).toBe(expected); - }); + it('builds fingerprints from valid entries only', () => { + const fingerprint = buildSkillFingerprint([ + { path: '', sha256: '' }, + { path: 'valid.txt', sha256: sha256Hex(strToU8('ok')) }, + ]); + const expected = buildSkillFingerprint([ + { path: 'valid.txt', sha256: sha256Hex(strToU8('ok')) }, + ]); + expect(fingerprint).toBe(expected); + }); - it("returns null for invalid skill origin metadata", async () => { - const workdir = await mkdtemp(join(tmpdir(), "dt-skill-origin-")); - expect(await readSkillOrigin(workdir)).toBeNull(); + it('returns null for invalid skill origin metadata', async () => { + const workdir = await mkdtemp(join(tmpdir(), 'dt-skill-origin-')); + expect(await readSkillOrigin(workdir)).toBeNull(); - await mkdir(join(workdir, ".dt-skill"), { recursive: true }); - await writeFile( - join(workdir, ".dt-skill", "origin.json"), - JSON.stringify({ version: 2 }), - "utf8", - ); - expect(await readSkillOrigin(workdir)).toBeNull(); + await mkdir(join(workdir, '.dt-skill'), { recursive: true }); + await writeFile( + join(workdir, '.dt-skill', 'origin.json'), + JSON.stringify({ version: 2 }), + 'utf8' + ); + expect(await readSkillOrigin(workdir)).toBeNull(); - await writeFile( - join(workdir, ".dt-skill", "origin.json"), - JSON.stringify({ version: 1, registry: "demo", slug: "x", installedAt: 1 }), - "utf8", - ); - expect(await readSkillOrigin(workdir)).toBeNull(); + await writeFile( + join(workdir, '.dt-skill', 'origin.json'), + JSON.stringify({ version: 1, registry: 'demo', slug: 'x', installedAt: 1 }), + 'utf8' + ); + expect(await readSkillOrigin(workdir)).toBeNull(); - await writeFile( - join(workdir, ".dt-skill", "origin.json"), - JSON.stringify({ - version: 1, - registry: "demo", - slug: "x", - installedVersion: "0.1.0", - installedAt: "nope", - }), - "utf8", - ); - expect(await readSkillOrigin(workdir)).toBeNull(); + await writeFile( + join(workdir, '.dt-skill', 'origin.json'), + JSON.stringify({ + version: 1, + registry: 'demo', + slug: 'x', + installedVersion: '0.1.0', + installedAt: 'nope', + }), + 'utf8' + ); + expect(await readSkillOrigin(workdir)).toBeNull(); - const origin: SkillOrigin = { - version: 1, - registry: "https://example.com", - slug: "demo", - installedVersion: "1.2.3", - installedAt: 123, - }; - await writeSkillOrigin(workdir, origin); - expect(await readSkillOrigin(workdir)).toEqual(origin); - }); + const origin: SkillOrigin = { + version: 1, + registry: 'https://example.com', + slug: 'demo', + installedVersion: '1.2.3', + installedAt: 123, + }; + await writeSkillOrigin(workdir, origin); + expect(await readSkillOrigin(workdir)).toEqual(origin); + }); - describe("listManualSkills", () => { - it("lists manual skills not present in the lockfile", async () => { - const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-")); - await mkdir(join(dir, "manual-skill")); - await writeFile(join(dir, "manual-skill", "SKILL.md"), "# Manual", "utf8"); + describe('listManualSkills', () => { + it('lists manual skills not present in the lockfile', async () => { + const dir = await mkdtemp(join(tmpdir(), 'dt-skill-manual-')); + await mkdir(join(dir, 'manual-skill')); + await writeFile(join(dir, 'manual-skill', 'SKILL.md'), '# Manual', 'utf8'); - await mkdir(join(dir, "tracked-skill")); - await writeFile(join(dir, "tracked-skill", "SKILL.md"), "# Tracked", "utf8"); + await mkdir(join(dir, 'tracked-skill')); + await writeFile(join(dir, 'tracked-skill', 'SKILL.md'), '# Tracked', 'utf8'); - const result = await listManualSkills(dir, new Set(["tracked-skill"])); - expect(result).toEqual(["manual-skill"]); - }); + const result = await listManualSkills(dir, new Set(['tracked-skill'])); + expect(result).toEqual(['manual-skill']); + }); - it("recognizes skills from origin metadata", async () => { - const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-origin-")); - await mkdir(join(dir, "with-origin", ".dt-skill"), { recursive: true }); - await writeFile(join(dir, "with-origin", ".dt-skill", "origin.json"), "{}", "utf8"); + it('recognizes skills from origin metadata', async () => { + const dir = await mkdtemp(join(tmpdir(), 'dt-skill-manual-origin-')); + await mkdir(join(dir, 'with-origin', '.dt-skill'), { recursive: true }); + await writeFile(join(dir, 'with-origin', '.dt-skill', 'origin.json'), '{}', 'utf8'); - const result = await listManualSkills(dir, new Set()); - expect(result).toEqual(["with-origin"]); - }); + const result = await listManualSkills(dir, new Set()); + expect(result).toEqual(['with-origin']); + }); - it("skips hidden and non-skill directories and returns sorted results", async () => { - const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-sort-")); - await mkdir(join(dir, "z-skill")); - await writeFile(join(dir, "z-skill", "SKILL.md"), "# Z", "utf8"); - await mkdir(join(dir, "a-skill")); - await writeFile(join(dir, "a-skill", "SKILL.md"), "# A", "utf8"); - await mkdir(join(dir, ".hidden")); - await writeFile(join(dir, ".hidden", "SKILL.md"), "# Hidden", "utf8"); - await mkdir(join(dir, "notes")); - await writeFile(join(dir, "notes", "README.md"), "not a skill", "utf8"); + it('skips hidden and non-skill directories and returns sorted results', async () => { + const dir = await mkdtemp(join(tmpdir(), 'dt-skill-manual-sort-')); + await mkdir(join(dir, 'z-skill')); + await writeFile(join(dir, 'z-skill', 'SKILL.md'), '# Z', 'utf8'); + await mkdir(join(dir, 'a-skill')); + await writeFile(join(dir, 'a-skill', 'SKILL.md'), '# A', 'utf8'); + await mkdir(join(dir, '.hidden')); + await writeFile(join(dir, '.hidden', 'SKILL.md'), '# Hidden', 'utf8'); + await mkdir(join(dir, 'notes')); + await writeFile(join(dir, 'notes', 'README.md'), 'not a skill', 'utf8'); - const result = await listManualSkills(dir, new Set()); - expect(result).toEqual(["a-skill", "z-skill"]); - }); + const result = await listManualSkills(dir, new Set()); + expect(result).toEqual(['a-skill', 'z-skill']); + }); - it("returns an empty list when the skills directory does not exist", async () => { - const dir = await mkdtemp(join(tmpdir(), "dt-skill-manual-missing-")); - const result = await listManualSkills(join(dir, "missing"), new Set()); - expect(result).toEqual([]); + it('returns an empty list when the skills directory does not exist', async () => { + const dir = await mkdtemp(join(tmpdir(), 'dt-skill-manual-missing-')); + const result = await listManualSkills(join(dir, 'missing'), new Set()); + expect(result).toEqual([]); + }); }); - }); }); diff --git a/dt-skill/src/skills.ts b/dt-skill/src/skills.ts index e4ea7b63..428bda9a 100644 --- a/dt-skill/src/skills.ts +++ b/dt-skill/src/skills.ts @@ -1,81 +1,77 @@ -import { access, mkdir, open, readdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, join, relative, resolve, sep } from "node:path"; -import { unzipSync } from "fflate"; -import ignore from "ignore"; -import mime from "mime"; +import { access, mkdir, open, readdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { unzipSync } from 'fflate'; +import ignore from 'ignore'; +import mime from 'mime'; +import { type Lockfile, LockfileSchema, parseArk } from './schema/index.js'; import { - type Lockfile, - LockfileSchema, - parseArk, -} from "./schema/index.js"; -import { - buildSkillFingerprint, - getFileExtension, - hasDotPathSegment, - isLikelyTextBytes, - sha256Hex, - shouldIncludeFingerprintFile, - TEXT_FILE_EXTENSION_SET, - TEXT_SAMPLE_BYTES, -} from "./schema/skillFingerprintContract.js"; + buildSkillFingerprint, + getFileExtension, + hasDotPathSegment, + isLikelyTextBytes, + sha256Hex, + shouldIncludeFingerprintFile, + TEXT_FILE_EXTENSION_SET, + TEXT_SAMPLE_BYTES, +} from './schema/skillFingerprintContract.js'; -const DOT_DIR = ".dt-skill"; -const DOT_IGNORE = ".dt-skillignore"; +const DOT_DIR = '.dt-skill'; +const DOT_IGNORE = '.dt-skillignore'; export type SkillOrigin = { - version: 1; - registry: string; - slug: string; - installedVersion: string; - installedAt: number; - fingerprint?: string; + version: 1; + registry: string; + slug: string; + installedVersion: string; + installedAt: number; + fingerprint?: string; }; export async function extractZipToDir(zipBytes: Uint8Array, targetDir: string) { - const entries = unzipSync(zipBytes); - await mkdir(targetDir, { recursive: true }); - for (const [rawPath, data] of Object.entries(entries)) { - const safePath = sanitizeRelPath(rawPath); - if (!safePath) continue; - const outPath = join(targetDir, safePath); - await mkdir(dirname(outPath), { recursive: true }); - await writeFile(outPath, data); - } + const entries = unzipSync(zipBytes); + await mkdir(targetDir, { recursive: true }); + for (const [rawPath, data] of Object.entries(entries)) { + const safePath = sanitizeRelPath(rawPath); + if (!safePath) continue; + const outPath = join(targetDir, safePath); + await mkdir(dirname(outPath), { recursive: true }); + await writeFile(outPath, data); + } } export async function listTextFiles(root: string) { - const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; - const { absRoot, ig } = await createSkillIgnoreMatcher(root); + const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; + const { absRoot, ig } = await createSkillIgnoreMatcher(root); - await walk(absRoot, async (absPath) => { - const relPath = normalizePath(relative(absRoot, absPath)); - if (!relPath) return; - if (ig.ignores(relPath)) return; - if (hasDotPathSegment(relPath)) return; - const ext = getFileExtension(relPath); - if (ext && !TEXT_FILE_EXTENSION_SET.has(ext)) return; - if (!ext && !(await isLikelyTextFile(absPath))) return; - const buffer = await readFile(absPath); - const contentType = mime.getType(relPath) ?? "text/plain"; - files.push({ relPath, bytes: new Uint8Array(buffer), contentType }); - }); - return files; + await walk(absRoot, async (absPath) => { + const relPath = normalizePath(relative(absRoot, absPath)); + if (!relPath) return; + if (ig.ignores(relPath)) return; + if (hasDotPathSegment(relPath)) return; + const ext = getFileExtension(relPath); + if (ext && !TEXT_FILE_EXTENSION_SET.has(ext)) return; + if (!ext && !(await isLikelyTextFile(absPath))) return; + const buffer = await readFile(absPath); + const contentType = mime.getType(relPath) ?? 'text/plain'; + files.push({ relPath, bytes: new Uint8Array(buffer), contentType }); + }); + return files; } export async function listPublishFiles(root: string) { - const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; - const { absRoot, ig } = await createSkillIgnoreMatcher(root); + const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []; + const { absRoot, ig } = await createSkillIgnoreMatcher(root); - await walk(absRoot, async (absPath) => { - const relPath = normalizePath(relative(absRoot, absPath)); - if (!relPath) return; - if (ig.ignores(relPath)) return; - if (hasDotPathSegment(relPath)) return; - const buffer = await readFile(absPath); - const contentType = mime.getType(relPath) ?? "application/octet-stream"; - files.push({ relPath, bytes: new Uint8Array(buffer), contentType }); - }); - return files; + await walk(absRoot, async (absPath) => { + const relPath = normalizePath(relative(absRoot, absPath)); + if (!relPath) return; + if (ig.ignores(relPath)) return; + if (hasDotPathSegment(relPath)) return; + const buffer = await readFile(absPath); + const contentType = mime.getType(relPath) ?? 'application/octet-stream'; + files.push({ relPath, bytes: new Uint8Array(buffer), contentType }); + }); + return files; } type SkillFileHash = { path: string; sha256: string; size: number }; @@ -83,181 +79,178 @@ type SkillFileHash = { path: string; sha256: string; size: number }; export { buildSkillFingerprint, sha256Hex }; export function hashSkillFiles(files: Array<{ relPath: string; bytes: Uint8Array }>) { - const hashed = files.map((file) => ({ - path: file.relPath, - sha256: sha256Hex(file.bytes), - size: file.bytes.byteLength, - })); - return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; + const hashed = files.map((file) => ({ + path: file.relPath, + sha256: sha256Hex(file.bytes), + size: file.bytes.byteLength, + })); + return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; } export function hashSkillZip(zipBytes: Uint8Array) { - const entries = unzipSync(zipBytes); - const hashed = Object.entries(entries) - .map(([rawPath, bytes]) => { - const safePath = sanitizeZipPath(rawPath); - if (!safePath) return null; - if ( - !shouldIncludeFingerprintFile({ - filePath: safePath, - bytes, + const entries = unzipSync(zipBytes); + const hashed = Object.entries(entries) + .map(([rawPath, bytes]) => { + const safePath = sanitizeZipPath(rawPath); + if (!safePath) return null; + if ( + !shouldIncludeFingerprintFile({ + filePath: safePath, + bytes, + }) + ) { + return null; + } + return { path: safePath, sha256: sha256Hex(bytes), size: bytes.byteLength }; }) - ) { - return null; - } - return { path: safePath, sha256: sha256Hex(bytes), size: bytes.byteLength }; - }) - .filter(Boolean) as SkillFileHash[]; + .filter(Boolean) as SkillFileHash[]; - return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; + return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }; } export async function readLockfile(workdir: string): Promise { - const path = join(workdir, DOT_DIR, "lock.json"); - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON.parse(raw) as unknown; - return parseArk(LockfileSchema, parsed, "Lockfile"); - } catch { - return { version: 1, skills: {} }; - } + const path = join(workdir, DOT_DIR, 'lock.json'); + try { + const raw = await readFile(path, 'utf8'); + const parsed = JSON.parse(raw) as unknown; + return parseArk(LockfileSchema, parsed, 'Lockfile'); + } catch { + return { version: 1, skills: {} }; + } } export async function writeLockfile(workdir: string, lock: Lockfile) { - const path = join(workdir, DOT_DIR, "lock.json"); - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, "utf8"); + const path = join(workdir, DOT_DIR, 'lock.json'); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, 'utf8'); } export async function readSkillOrigin(skillFolder: string): Promise { - const path = join(skillFolder, DOT_DIR, "origin.json"); - try { - const raw = await readFile(path, "utf8"); - const parsed = JSON.parse(raw) as Partial; - if (parsed.version !== 1) return null; - if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null; - if (typeof parsed.installedAt !== "number" || !Number.isFinite(parsed.installedAt)) { - return null; + const path = join(skillFolder, DOT_DIR, 'origin.json'); + try { + const raw = await readFile(path, 'utf8'); + const parsed = JSON.parse(raw) as Partial; + if (parsed.version !== 1) return null; + if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null; + if (typeof parsed.installedAt !== 'number' || !Number.isFinite(parsed.installedAt)) { + return null; + } + return { + version: 1, + registry: parsed.registry, + slug: parsed.slug, + installedVersion: parsed.installedVersion, + installedAt: parsed.installedAt, + fingerprint: typeof parsed.fingerprint === 'string' ? parsed.fingerprint : undefined, + }; + } catch { + return null; } - return { - version: 1, - registry: parsed.registry, - slug: parsed.slug, - installedVersion: parsed.installedVersion, - installedAt: parsed.installedAt, - fingerprint: typeof parsed.fingerprint === "string" ? parsed.fingerprint : undefined, - }; - } catch { - return null; - } } export async function writeSkillOrigin(skillFolder: string, origin: SkillOrigin) { - const path = join(skillFolder, DOT_DIR, "origin.json"); - await mkdir(dirname(path), { recursive: true }); - await writeFile(path, `${JSON.stringify(origin, null, 2)}\n`, "utf8"); + const path = join(skillFolder, DOT_DIR, 'origin.json'); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, `${JSON.stringify(origin, null, 2)}\n`, 'utf8'); } function normalizePath(path: string) { - return path - .split(sep) - .join("/") - .replace(/^\.\/+/, ""); + return path + .split(sep) + .join('/') + .replace(/^\.\/+/, ''); } async function isLikelyTextFile(path: string) { - const handle = await open(path, "r"); - try { - const sample = new Uint8Array(TEXT_SAMPLE_BYTES); - const { bytesRead } = await handle.read(sample, 0, sample.byteLength, 0); - return isLikelyTextBytes(sample.subarray(0, bytesRead)); - } finally { - await handle.close(); - } + const handle = await open(path, 'r'); + try { + const sample = new Uint8Array(TEXT_SAMPLE_BYTES); + const { bytesRead } = await handle.read(sample, 0, sample.byteLength, 0); + return isLikelyTextBytes(sample.subarray(0, bytesRead)); + } finally { + await handle.close(); + } } function sanitizeRelPath(path: string) { - const normalized = path.replace(/^\.\/+/, "").replace(/^\/+/, ""); - if (!normalized || normalized.endsWith("/")) return null; - if (normalized.includes("..") || normalized.includes("\\")) return null; - return normalized; + const normalized = path.replace(/^\.\/+/, '').replace(/^\/+/, ''); + if (!normalized || normalized.endsWith('/')) return null; + if (normalized.includes('..') || normalized.includes('\\')) return null; + return normalized; } function sanitizeZipPath(path: string) { - return sanitizeRelPath(path); + return sanitizeRelPath(path); } async function walk(dir: string, onFile: (path: string) => Promise) { - const entries = await readdir(dir, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name.startsWith(".")) continue; - if (entry.name === "node_modules") continue; - const full = join(dir, entry.name); - if (entry.isDirectory()) { - await walk(full, onFile); - continue; + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith('.')) continue; + if (entry.name === 'node_modules') continue; + const full = join(dir, entry.name); + if (entry.isDirectory()) { + await walk(full, onFile); + continue; + } + if (!entry.isFile()) continue; + await onFile(full); } - if (!entry.isFile()) continue; - await onFile(full); - } } async function addIgnoreFile(ig: ReturnType, path: string) { - try { - const raw = await readFile(path, "utf8"); - ig.add(raw.split(/\r?\n/)); - } catch { - // optional - } + try { + const raw = await readFile(path, 'utf8'); + ig.add(raw.split(/\r?\n/)); + } catch { + // optional + } } async function createSkillIgnoreMatcher(root: string) { - const absRoot = resolve(root); - const ig = ignore(); - ig.add([".git/", "node_modules/", `${DOT_DIR}/`]); - await addIgnoreFile(ig, join(absRoot, ".gitignore")); - await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); - return { absRoot, ig }; + const absRoot = resolve(root); + const ig = ignore(); + ig.add(['.git/', 'node_modules/', `${DOT_DIR}/`]); + await addIgnoreFile(ig, join(absRoot, '.gitignore')); + await addIgnoreFile(ig, join(absRoot, DOT_IGNORE)); + return { absRoot, ig }; } export async function listManualSkills(skillsDir: string, lockedSlugs: Set) { - const manual: string[] = []; - let entries; - try { - entries = await readdir(skillsDir, { withFileTypes: true }); - } catch (error) { - if (isMissingPathError(error)) return manual; - throw error; - } + const manual: string[] = []; + let entries; + try { + entries = await readdir(skillsDir, { withFileTypes: true }); + } catch (error) { + if (isMissingPathError(error)) return manual; + throw error; + } - for (const entry of entries) { - if (!entry.isDirectory()) continue; - if (entry.name.startsWith(".")) continue; - if (lockedSlugs.has(entry.name)) continue; - if (await hasSkillMetadata(join(skillsDir, entry.name))) { - manual.push(entry.name); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (entry.name.startsWith('.')) continue; + if (lockedSlugs.has(entry.name)) continue; + if (await hasSkillMetadata(join(skillsDir, entry.name))) { + manual.push(entry.name); + } } - } - return manual.sort((a, b) => a.localeCompare(b)); + return manual.sort((a, b) => a.localeCompare(b)); } async function hasSkillMetadata(skillDir: string) { - const candidates = [ - join(skillDir, "SKILL.md"), - join(skillDir, DOT_DIR, "origin.json"), - ]; - for (const path of candidates) { - try { - await access(path); - return true; - } catch (error) { - if (!isMissingPathError(error)) throw error; + const candidates = [join(skillDir, 'SKILL.md'), join(skillDir, DOT_DIR, 'origin.json')]; + for (const path of candidates) { + try { + await access(path); + return true; + } catch (error) { + if (!isMissingPathError(error)) throw error; + } } - } - return false; + return false; } function isMissingPathError(error: unknown) { - const code = (error as NodeJS.ErrnoException | undefined)?.code; - return code === "ENOENT" || code === "ENOTDIR"; + const code = (error as NodeJS.ErrnoException | undefined)?.code; + return code === 'ENOENT' || code === 'ENOTDIR'; } diff --git a/dt-skill/test-artifact/cli.artifact.test.ts b/dt-skill/test-artifact/cli.artifact.test.ts index 7c25b0fc..24c3551c 100644 --- a/dt-skill/test-artifact/cli.artifact.test.ts +++ b/dt-skill/test-artifact/cli.artifact.test.ts @@ -54,7 +54,10 @@ describe('built CLI artifact', () => { [ '--input-type=module', '--eval', - `import('${join(isolatedPackage, 'dist/schema/skillFingerprintContract.js').replaceAll('\\', '\\\\')}')`, + `import('${join( + isolatedPackage, + 'dist/schema/skillFingerprintContract.js' + ).replaceAll('\\', '\\\\')}')`, ], { encoding: 'utf8' } ); diff --git a/dt-skill/test/cliCommandTestKit.ts b/dt-skill/test/cliCommandTestKit.ts index aee73f3e..8dba8df2 100644 --- a/dt-skill/test/cliCommandTestKit.ts +++ b/dt-skill/test/cliCommandTestKit.ts @@ -1,87 +1,88 @@ -import { join } from "node:path"; -import { vi } from "vitest"; -import type { GlobalOpts } from "../src/cli/types.js"; +import { join } from 'node:path'; +import { vi } from 'vitest'; +import type { GlobalOpts } from '../src/cli/types.js'; -export function makeGlobalOpts(workdir = "/work"): GlobalOpts { - return { - workdir, - dir: join(workdir, "skills"), - site: "https://example.com", - registry: "https://example.com", - registrySource: "default", - }; +export function makeGlobalOpts(workdir = '/work'): GlobalOpts { + return { + workdir, + dir: join(workdir, 'skills'), + site: 'https://example.com', + registry: 'https://example.com', + registrySource: 'default', + }; } function buildRegistryUrl(path: string, registry: string) { - const base = registry.endsWith("/") ? registry : `${registry}/`; - const relative = path.startsWith("/") ? path.slice(1) : path; - return new URL(relative, base); + const base = registry.endsWith('/') ? registry : `${registry}/`; + const relative = path.startsWith('/') ? path.slice(1) : path; + return new URL(relative, base); } export function createHttpModuleMocks() { - const apiRequest = vi.fn(); - const apiRequestForm = vi.fn(); - const downloadZip = vi.fn(); - const fetchBinary = vi.fn(); - const fetchText = vi.fn(); - const registryUrl = vi.fn(buildRegistryUrl); + const apiRequest = vi.fn(); + const apiRequestForm = vi.fn(); + const downloadZip = vi.fn(); + const fetchBinary = vi.fn(); + const fetchText = vi.fn(); + const registryUrl = vi.fn(buildRegistryUrl); - return { - apiRequest, - apiRequestForm, - downloadZip, - fetchBinary, - fetchText, - registryUrl, - moduleFactory: () => ({ - apiRequest: (registry: unknown, args: unknown, schema?: unknown) => - apiRequest(registry, args, schema), - apiRequestForm: (registry: unknown, args: unknown, schema?: unknown) => - apiRequestForm(registry, args, schema), - downloadZip: (registry: unknown, args: unknown) => downloadZip(registry, args), - fetchBinary: (registry: unknown, args: unknown) => fetchBinary(registry, args), - fetchText: (registry: unknown, args: unknown) => fetchText(registry, args), - registryUrl: (...args: [string, string]) => registryUrl(...args), - }), - }; + return { + apiRequest, + apiRequestForm, + downloadZip, + fetchBinary, + fetchText, + registryUrl, + moduleFactory: () => ({ + apiRequest: (registry: unknown, args: unknown, schema?: unknown) => + apiRequest(registry, args, schema), + apiRequestForm: (registry: unknown, args: unknown, schema?: unknown) => + apiRequestForm(registry, args, schema), + downloadZip: (registry: unknown, args: unknown) => downloadZip(registry, args), + fetchBinary: (registry: unknown, args: unknown) => fetchBinary(registry, args), + fetchText: (registry: unknown, args: unknown) => fetchText(registry, args), + registryUrl: (...args: [string, string]) => registryUrl(...args), + }), + }; } export function createRegistryModuleMocks() { - const getRegistry = vi.fn(async (_opts?: unknown, _params?: unknown) => "https://example.com"); + const getRegistry = vi.fn(async (_opts?: unknown, _params?: unknown) => 'https://example.com'); - return { - getRegistry, - moduleFactory: () => ({ - getRegistry: (opts: unknown, params?: unknown) => getRegistry(opts, params), - }), - }; + return { + getRegistry, + moduleFactory: () => ({ + getRegistry: (opts: unknown, params?: unknown) => getRegistry(opts, params), + }), + }; } export function createUiModuleMocks(options?: { interactive?: boolean }) { - const spinner = { - stop: vi.fn(), - fail: vi.fn(), - succeed: vi.fn(), - start: vi.fn(), - isSpinning: false, - text: "", - }; - const fail = vi.fn((message: string) => { - throw new Error(message); - }); - const promptConfirm = vi.fn(async () => true); - const interactive = options?.interactive ?? false; + const spinner = { + stop: vi.fn(), + fail: vi.fn(), + succeed: vi.fn(), + start: vi.fn(), + isSpinning: false, + text: '', + }; + const fail = vi.fn((message: string) => { + throw new Error(message); + }); + const promptConfirm = vi.fn(async () => true); + const interactive = options?.interactive ?? false; - return { - spinner, - fail, - promptConfirm, - moduleFactory: () => ({ - createSpinner: vi.fn(() => spinner), - fail: (message: string) => fail(message), - formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)), - isInteractive: () => interactive, - promptConfirm, - }), - }; + return { + spinner, + fail, + promptConfirm, + moduleFactory: () => ({ + createSpinner: vi.fn(() => spinner), + fail: (message: string) => fail(message), + formatError: (error: unknown) => + error instanceof Error ? error.message : String(error), + isInteractive: () => interactive, + promptConfirm, + }), + }; } diff --git a/dt-skill/test/runtimeStubs.ts b/dt-skill/test/runtimeStubs.ts index dc359a7a..3a7d35e8 100644 --- a/dt-skill/test/runtimeStubs.ts +++ b/dt-skill/test/runtimeStubs.ts @@ -1,54 +1,54 @@ export function createGlobalStubRegistry() { - const restorers: Array<() => void> = []; + const restorers: Array<() => void> = []; - return { - stub(name: K, value: (typeof globalThis)[K]) { - const original = globalThis[name]; - restorers.push(() => { - if (original === undefined) { - Reflect.deleteProperty(globalThis, name); - return; - } - Object.defineProperty(globalThis, name, { - configurable: true, - writable: true, - value: original, - }); - }); - Object.defineProperty(globalThis, name, { - configurable: true, - writable: true, - value, - }); - }, - restoreAll() { - while (restorers.length > 0) { - restorers.pop()?.(); - } - }, - }; + return { + stub(name: K, value: typeof globalThis[K]) { + const original = globalThis[name]; + restorers.push(() => { + if (original === undefined) { + Reflect.deleteProperty(globalThis, name); + return; + } + Object.defineProperty(globalThis, name, { + configurable: true, + writable: true, + value: original, + }); + }); + Object.defineProperty(globalThis, name, { + configurable: true, + writable: true, + value, + }); + }, + restoreAll() { + while (restorers.length > 0) { + restorers.pop()?.(); + } + }, + }; } export function createEnvStubRegistry() { - const restorers: Array<() => void> = []; + const restorers: Array<() => void> = []; - return { - stub(name: string, value: string) { - const original = process.env[name]; - const hadOriginal = Object.prototype.hasOwnProperty.call(process.env, name); - restorers.push(() => { - if (hadOriginal) { - process.env[name] = original; - return; - } - delete process.env[name]; - }); - process.env[name] = value; - }, - restoreAll() { - while (restorers.length > 0) { - restorers.pop()?.(); - } - }, - }; + return { + stub(name: string, value: string) { + const original = process.env[name]; + const hadOriginal = Object.prototype.hasOwnProperty.call(process.env, name); + restorers.push(() => { + if (hadOriginal) { + process.env[name] = original; + return; + } + delete process.env[name]; + }); + process.env[name] = value; + }, + restoreAll() { + while (restorers.length > 0) { + restorers.pop()?.(); + } + }, + }; } diff --git a/dt-skill/tsconfig.json b/dt-skill/tsconfig.json index 25581cbe..d79ccfae 100644 --- a/dt-skill/tsconfig.json +++ b/dt-skill/tsconfig.json @@ -1,15 +1,15 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "Bundler", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "declaration": true, - "sourceMap": true, - "skipLibCheck": true - }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.test.ts"] + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "declaration": true, + "sourceMap": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts"] } diff --git a/dt-skill/vitest.artifact.config.ts b/dt-skill/vitest.artifact.config.ts index bf44a678..29028dc4 100644 --- a/dt-skill/vitest.artifact.config.ts +++ b/dt-skill/vitest.artifact.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from 'vitest/config'; export default defineConfig({ - test: { - environment: "node", - globals: false, - testTimeout: 30_000, - hookTimeout: 30_000, - include: ["test-artifact/**/*.test.ts"], - exclude: ["dist/**", "node_modules/**"], - }, + test: { + environment: 'node', + globals: false, + testTimeout: 30_000, + hookTimeout: 30_000, + include: ['test-artifact/**/*.test.ts'], + exclude: ['dist/**', 'node_modules/**'], + }, }); diff --git a/dt-skill/vitest.config.ts b/dt-skill/vitest.config.ts index 04b54e23..3a7d653f 100644 --- a/dt-skill/vitest.config.ts +++ b/dt-skill/vitest.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from 'vitest/config'; export default defineConfig({ - test: { - environment: "node", - globals: false, - testTimeout: 15_000, - hookTimeout: 15_000, - include: ["src/**/*.test.ts"], - exclude: ["dist/**", "node_modules/**", "test-artifact/**"], - }, + test: { + environment: 'node', + globals: false, + testTimeout: 15_000, + hookTimeout: 15_000, + include: ['src/**/*.test.ts'], + exclude: ['dist/**', 'node_modules/**', 'test-artifact/**'], + }, }); diff --git a/test/github-stars.test.js b/test/github-stars.test.js index 0a570935..0d0d46f8 100644 --- a/test/github-stars.test.js +++ b/test/github-stars.test.js @@ -5,14 +5,23 @@ const GitHubStarsClient = require('../app/utils/github-stars'); test('extractGitHubRepoFullName parses SSH git@github.com URLs', () => { const client = new GitHubStarsClient(); - assert.equal(client.extractGitHubRepoFullName('git@github.com:anthropics/claude-code.git'), 'anthropics/claude-code'); + assert.equal( + client.extractGitHubRepoFullName('git@github.com:anthropics/claude-code.git'), + 'anthropics/claude-code' + ); assert.equal(client.extractGitHubRepoFullName('git@github.com:owner/repo'), 'owner/repo'); }); test('extractGitHubRepoFullName parses HTTPS github.com URLs', () => { const client = new GitHubStarsClient(); - assert.equal(client.extractGitHubRepoFullName('https://github.com/facebook/react'), 'facebook/react'); - assert.equal(client.extractGitHubRepoFullName('https://github.com/owner/repo.git'), 'owner/repo'); + assert.equal( + client.extractGitHubRepoFullName('https://github.com/facebook/react'), + 'facebook/react' + ); + assert.equal( + client.extractGitHubRepoFullName('https://github.com/owner/repo.git'), + 'owner/repo' + ); assert.equal(client.extractGitHubRepoFullName('http://github.com/owner/repo'), 'owner/repo'); }); diff --git a/test/skills-import-package-name.test.js b/test/skills-import-package-name.test.js index 3553d730..6a1a2bf3 100644 --- a/test/skills-import-package-name.test.js +++ b/test/skills-import-package-name.test.js @@ -33,8 +33,11 @@ test('getUploadIdentityKey returns empty string when no preferredName', () => { [{ name: 'skill-b' }, { name: 'skill-a' }], '' ); - assert.equal(result, '', - 'Without preferredName, should return empty string to keep package name clean'); + assert.equal( + result, + '', + 'Without preferredName, should return empty string to keep package name clean' + ); }); test('buildUploadSourceMeta with packageName as identityKey produces readable sourceKey', () => { @@ -52,14 +55,17 @@ test('buildUploadSourceMeta produces clean repoPath with or without packageName' 'demo-multi-skill-folders.zip', 'demo-multi-skill-folders' ); - const withoutPackage = skillsModule.prototype.buildUploadSourceMeta( - 'skills-batch.zip', - '' + const withoutPackage = skillsModule.prototype.buildUploadSourceMeta('skills-batch.zip', ''); + assert.equal( + withPackage.repoPath, + 'demo-multi-skill-folders', + 'With packageName, repoPath should equal packageName' + ); + assert.equal( + withoutPackage.repoPath, + 'skills-batch', + 'Without packageName, repoPath should equal zip filename' ); - assert.equal(withPackage.repoPath, 'demo-multi-skill-folders', - 'With packageName, repoPath should equal packageName'); - assert.equal(withoutPackage.repoPath, 'skills-batch', - 'Without packageName, repoPath should equal zip filename'); }); test('persistSkillsForSource accepts preferredPackageName parameter', () => { @@ -83,8 +89,11 @@ test('importSkillFile multi-skill upload uses clean zip filename as package name // First build: used for buildSkillSlug const parsedSource = service.buildUploadSourceMeta(fileName, identityKey); - assert.equal(parsedSource.repoPath, 'test-skill-package', - 'First build should have clean repoPath from filename'); + assert.equal( + parsedSource.repoPath, + 'test-skill-package', + 'First build should have clean repoPath from filename' + ); // Skill records from discoverSkillDirs + prepareSkillRecord const skillRecords = [{ name: 'web-scraper' }, { name: 'api-tester' }]; @@ -98,8 +107,11 @@ test('importSkillFile multi-skill upload uses clean zip filename as package name // BUG: currently produces 'test-skill-package-api-tester-web-scraper-b0c3619b' // EXPECTED: 'test-skill-package' (just the filename, readable) - assert.equal(uploadSourceMeta.repoPath, 'test-skill-package', - `Package name should be clean zip filename, got: ${uploadSourceMeta.repoPath}`); + assert.equal( + uploadSourceMeta.repoPath, + 'test-skill-package', + `Package name should be clean zip filename, got: ${uploadSourceMeta.repoPath}` + ); }); test('importSkillFile single-skill upload with custom name uses name in repoPath', () => { @@ -118,8 +130,11 @@ test('importSkillFile single-skill upload with custom name uses name in repoPath service.getUploadIdentityKey(skillRecords, identityKey) ); - assert.equal(uploadSourceMeta.repoPath, 'custom-skill-name', - 'Single-skill upload with custom name should use name as repoPath'); + assert.equal( + uploadSourceMeta.repoPath, + 'custom-skill-name', + 'Single-skill upload with custom name should use name as repoPath' + ); }); test('importSkillFile CLI upload with packageName uses packageName as repoPath', () => { @@ -138,6 +153,9 @@ test('importSkillFile CLI upload with packageName uses packageName as repoPath', service.getUploadIdentityKey(skillRecords, identityKey) ); - assert.equal(uploadSourceMeta.repoPath, 'my-awesome-pack', - 'CLI upload with packageName should use packageName as repoPath'); + assert.equal( + uploadSourceMeta.repoPath, + 'my-awesome-pack', + 'CLI upload with packageName should use packageName as repoPath' + ); }); diff --git a/test/skills-install-key.test.js b/test/skills-install-key.test.js index d520fefa..600b1a24 100644 --- a/test/skills-install-key.test.js +++ b/test/skills-install-key.test.js @@ -191,7 +191,7 @@ test('buildSkillSlug generates clean slug for upload source without prefix', () // 网页端上传,ZIP内技能目录为 "my-skill" const slug = service.buildSkillSlug(sourceMeta, 'my-skill', 'My Skill'); assert.equal(slug, 'my-skill'); - + // 如果 relativeSkillPath 是 ".",应该回退使用 skillName const slugDot = service.buildSkillSlug(sourceMeta, '.', 'My Skill'); assert.equal(slugDot, 'my-skill'); @@ -199,12 +199,12 @@ test('buildSkillSlug generates clean slug for upload source without prefix', () test('persistSkillsForSource - TDD scenarios for web upload', async () => { const service = Object.create(SkillsService.prototype); - + service.fetchStarsBySourceRepo = async () => 0; service.buildSkillSlug = SkillsService.prototype.buildSkillSlug; service.sanitizeSlugSegment = SkillsService.prototype.sanitizeSlugSegment; service.hashString = SkillsService.prototype.hashString; - + const dbSkills = []; const dbFiles = []; let idCounter = 1; @@ -212,11 +212,11 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { const SkillsItem = { findAll: async (options) => { const sourceId = options.where.source_id; - return dbSkills.filter(item => item.source_id === sourceId && item.is_delete === 0); + return dbSkills.filter((item) => item.source_id === sourceId && item.is_delete === 0); }, findOne: async (options) => { const slug = options.where.slug; - return dbSkills.find(item => item.slug === slug) || null; + return dbSkills.find((item) => item.slug === slug) || null; }, create: async (payload) => { const newItem = { @@ -231,7 +231,7 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { return newItem; }, }; - + const SkillsFile = { destroy: async () => {}, bulkCreate: async (rows) => { @@ -255,7 +255,7 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { }, }, }; - + service.ctx = { throw(status, message) { const error = new Error(message); @@ -285,7 +285,7 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { installCommand: '', files: [], }; - + const result1 = await service.persistSkillsForSource(10, sourceMeta, [record1]); assert.equal(result1.length, 1); assert.equal(result1[0].slug, 'my-skill'); @@ -316,7 +316,7 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { installCommand: '', files: [], }; - + await assert.rejects( () => service.persistSkillsForSource(11, sourceMeta, [recordConflict]), (error) => error.status === 400 && error.message === 'slug 已存在' @@ -324,7 +324,7 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { // 4. 对已软删除的同 slug 技能进行复用更新 dbSkills[0].is_delete = 1; - + const recordReuse = { name: 'reused-name', description: 'reused-desc', @@ -339,7 +339,7 @@ test('persistSkillsForSource - TDD scenarios for web upload', async () => { installCommand: '', files: [], }; - + const resultReuse = await service.persistSkillsForSource(12, sourceMeta, [recordReuse]); assert.equal(resultReuse.length, 1); assert.equal(dbSkills.length, 1); @@ -356,13 +356,17 @@ test('assertSkillNamesUnique with excludeSlugs option ignores matching slug', as throw error; }, }; - + service.app = { model: { SkillsItem: { findOne: async (options) => { const slugFilter = options.where.slug; - if (slugFilter && slugFilter['notIn'] && slugFilter['notIn'].includes('skill-a')) { + if ( + slugFilter && + slugFilter['notIn'] && + slugFilter['notIn'].includes('skill-a') + ) { return null; } return { id: 9, name: 'skill-a', slug: 'skill-a' }; @@ -405,15 +409,21 @@ test('persistSkillsForSource - auto-package for multiple skill records and query findAll: async (options) => { const sourceId = options.where.source_id; // If filtering out sub-skills, parent_slug must be null - let result = dbSkills.filter(item => item.source_id === sourceId && item.is_delete === 0); - if (options.where && 'parent_slug' in options.where && options.where.parent_slug === null) { - result = result.filter(item => item.parent_slug === null); + let result = dbSkills.filter( + (item) => item.source_id === sourceId && item.is_delete === 0 + ); + if ( + options.where && + 'parent_slug' in options.where && + options.where.parent_slug === null + ) { + result = result.filter((item) => item.parent_slug === null); } return result; }, findOne: async (options) => { const slug = options.where.slug; - return dbSkills.find(item => item.slug === slug) || null; + return dbSkills.find((item) => item.slug === slug) || null; }, create: async (payload) => { const newItem = { @@ -500,14 +510,14 @@ test('persistSkillsForSource - auto-package for multiple skill records and query ]; await service.persistSkillsForSource(20, sourceMeta, records); - + // There should be a parent package item (is_package = 1) and two sub-skills (parent_slug = parentSlug) - const parent = dbSkills.find(item => item.is_package === 1); + const parent = dbSkills.find((item) => item.is_package === 1); assert.ok(parent, 'Should create a parent package'); assert.equal(parent.name, 'my-skills-pack'); assert.equal(parent.is_package, 1); - const subSkills = dbSkills.filter(item => item.parent_slug === parent.slug); + const subSkills = dbSkills.filter((item) => item.parent_slug === parent.slug); assert.equal(subSkills.length, 2, 'Both sub-skills should refer to parent slug'); assert.equal(subSkills[0].is_package, 0); @@ -534,14 +544,10 @@ test('getSkillArchive - skill package ZIP nested structures (T008)', async () => SkillsFile: { findAll: async (options) => { if (options.where.skill_id === 101) { - return [ - { file_path: 'README.md', content: 'c3ViLTE=', is_binary: 0 }, - ]; + return [{ file_path: 'README.md', content: 'c3ViLTE=', is_binary: 0 }]; } if (options.where.skill_id === 102) { - return [ - { file_path: 'index.js', content: 'c3ViLTI=', is_binary: 0 }, - ]; + return [{ file_path: 'index.js', content: 'c3ViLTI=', is_binary: 0 }]; } return []; }, @@ -559,11 +565,11 @@ test('getSkillArchive - skill package ZIP nested structures (T008)', async () => const archive = await service.getSkillArchive('my-skills-pack'); assert.ok(archive.content); - + const AdmZip = require('adm-zip'); const zip = new AdmZip(archive.content); - const entries = zip.getEntries().map(e => e.entryName); - + const entries = zip.getEntries().map((e) => e.entryName); + assert.ok(entries.includes('my-skills-pack/sub-skill-1/README.md')); assert.ok(entries.includes('my-skills-pack/sub-skill-2/index.js')); }); @@ -593,7 +599,9 @@ test('ensureSkillsItemPackageColumns - auto-migration adds is_package and parent }, Sequelize: { TINYINT: 'TINYINT', - STRING(len) { this.len = len; }, + STRING(len) { + this.len = len; + }, }, }; @@ -626,7 +634,9 @@ test('ensureSkillsItemPackageColumns - skips migration when columns already exis }, Sequelize: { TINYINT: 'TINYINT', - STRING(len) { this.len = len; }, + STRING(len) { + this.len = len; + }, }, }; @@ -646,13 +656,16 @@ test('persistSkillsForSource - single skill source does not create package (T015 let idCounter = 1; const SkillsItem = { - findAll: async () => dbSkills.filter(item => item.is_delete === 0), - findOne: async (opts) => dbSkills.find(item => item.slug === opts.where.slug) || null, + findAll: async () => dbSkills.filter((item) => item.is_delete === 0), + findOne: async (opts) => dbSkills.find((item) => item.slug === opts.where.slug) || null, create: async (payload) => { const newItem = { id: idCounter++, ...payload, - update: async (fields) => { Object.assign(newItem, fields); return newItem; }, + update: async (fields) => { + Object.assign(newItem, fields); + return newItem; + }, }; dbSkills.push(newItem); return newItem; @@ -674,7 +687,11 @@ test('persistSkillsForSource - single skill source does not create package (T015 }; service.ctx = { - throw(status, message) { const e = new Error(message); e.status = status; throw e; }, + throw(status, message) { + const e = new Error(message); + e.status = status; + throw e; + }, }; const sourceMeta = { @@ -683,20 +700,22 @@ test('persistSkillsForSource - single skill source does not create package (T015 repoPath: 'single-skill-zip', }; - const singleRecord = [{ - name: 'solo-skill', - description: 'a single skill', - category: '通用', - version: '1.0.0', - tags: [], - allowedTools: [], - updatedAt: new Date(), - sourceRepo: '', - sourcePath: 'solo-skill', - skillMd: '# solo', - installCommand: '', - files: [], - }]; + const singleRecord = [ + { + name: 'solo-skill', + description: 'a single skill', + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + updatedAt: new Date(), + sourceRepo: '', + sourcePath: 'solo-skill', + skillMd: '# solo', + installCommand: '', + files: [], + }, + ]; await service.persistSkillsForSource(30, sourceMeta, singleRecord); @@ -719,14 +738,17 @@ test('persistSkillsForSource - multi-skill source creates parent package with ch const SkillsItem = { findAll: async (opts) => { const sourceId = opts.where.source_id; - return dbSkills.filter(item => item.source_id === sourceId && item.is_delete === 0); + return dbSkills.filter((item) => item.source_id === sourceId && item.is_delete === 0); }, - findOne: async (opts) => dbSkills.find(item => item.slug === opts.where.slug) || null, + findOne: async (opts) => dbSkills.find((item) => item.slug === opts.where.slug) || null, create: async (payload) => { const newItem = { id: idCounter++, ...payload, - update: async (fields) => { Object.assign(newItem, fields); return newItem; }, + update: async (fields) => { + Object.assign(newItem, fields); + return newItem; + }, }; dbSkills.push(newItem); return newItem; @@ -748,7 +770,11 @@ test('persistSkillsForSource - multi-skill source creates parent package with ch }; service.ctx = { - throw(status, message) { const e = new Error(message); e.status = status; throw e; }, + throw(status, message) { + const e = new Error(message); + e.status = status; + throw e; + }, }; const sourceMeta = { @@ -804,8 +830,8 @@ test('persistSkillsForSource - multi-skill source creates parent package with ch await service.persistSkillsForSource(40, sourceMeta, multiRecords); - const parents = dbSkills.filter(s => s.is_package === 1); - const children = dbSkills.filter(s => s.parent_slug !== null && s.parent_slug !== ''); + const parents = dbSkills.filter((s) => s.is_package === 1); + const children = dbSkills.filter((s) => s.parent_slug !== null && s.parent_slug !== ''); assert.equal(parents.length, 1, 'Should create exactly 1 parent package'); assert.equal(children.length, 3, 'Should create exactly 3 child skills'); @@ -815,10 +841,17 @@ test('persistSkillsForSource - multi-skill source creates parent package with ch assert.equal(parent.parent_slug, null, 'Parent should have null parent_slug'); assert.equal(parent.name, 'mega-pack'); assert.equal(parent.source_path, '.'); - assert.ok(parent.description.includes('alpha-skill'), 'Parent description should list children'); + assert.ok( + parent.description.includes('alpha-skill'), + 'Parent description should list children' + ); for (const child of children) { - assert.equal(child.parent_slug, parent.slug, `Child ${child.slug} should reference parent slug`); + assert.equal( + child.parent_slug, + parent.slug, + `Child ${child.slug} should reference parent slug` + ); assert.equal(child.is_package, 0, `Child ${child.slug} should NOT be a package`); } }); @@ -847,8 +880,28 @@ test('getSkillDetail returns children for package, none for regular skill (T018) const service = Object.create(SkillsService.prototype); const mockChildren = [ - { id: 201, slug: 'sub-1', name: 'Sub 1', is_package: 0, parent_slug: 'test-pack', is_delete: 0, stars: 0, updated_at_remote: new Date(), updated_at: new Date() }, - { id: 202, slug: 'sub-2', name: 'Sub 2', is_package: 0, parent_slug: 'test-pack', is_delete: 0, stars: 0, updated_at_remote: new Date(), updated_at: new Date() }, + { + id: 201, + slug: 'sub-1', + name: 'Sub 1', + is_package: 0, + parent_slug: 'test-pack', + is_delete: 0, + stars: 0, + updated_at_remote: new Date(), + updated_at: new Date(), + }, + { + id: 202, + slug: 'sub-2', + name: 'Sub 2', + is_package: 0, + parent_slug: 'test-pack', + is_delete: 0, + stars: 0, + updated_at_remote: new Date(), + updated_at: new Date(), + }, ]; service.app = { @@ -870,12 +923,48 @@ test('getSkillDetail returns children for package, none for regular skill (T018) service.ensureSkillCache = async () => {}; service.getSkillByIdentifier = (_slug) => { if (_slug === 'test-pack') { - return { id: 100, slug: 'test-pack', name: 'Test Pack', isPackage: 1, parentSlug: null, skillMd: '# pack', stars: 10, category: '通用', version: '1.0.0', tags: [], allowedTools: [], sourceRepo: '', sourcePath: '.', description: 'test' }; + return { + id: 100, + slug: 'test-pack', + name: 'Test Pack', + isPackage: 1, + parentSlug: null, + skillMd: '# pack', + stars: 10, + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + sourceRepo: '', + sourcePath: '.', + description: 'test', + }; } - return { id: 200, slug: 'solo', name: 'Solo', isPackage: 0, parentSlug: null, skillMd: '# solo', stars: 5, category: '通用', version: '1.0.0', tags: [], allowedTools: [], sourceRepo: '', sourcePath: '.', description: 'test' }; + return { + id: 200, + slug: 'solo', + name: 'Solo', + isPackage: 0, + parentSlug: null, + skillMd: '# solo', + stars: 5, + category: '通用', + version: '1.0.0', + tags: [], + allowedTools: [], + sourceRepo: '', + sourcePath: '.', + description: 'test', + }; }; service.toSkillDto = (row) => row; - service.toPublicSkill = (s) => ({ slug: s.slug, name: s.name, isPackage: s.isPackage || s.is_package, parentSlug: s.parentSlug || s.parent_slug, description: s.description || '' }); + service.toPublicSkill = (s) => ({ + slug: s.slug, + name: s.name, + isPackage: s.isPackage || s.is_package, + parentSlug: s.parentSlug || s.parent_slug, + description: s.description || '', + }); // Package detail should include children const packDetail = await service.getSkillDetail('test-pack'); @@ -947,4 +1036,3 @@ test('getSkillArchive - nested ZIP structure for package with multiple children assert.ok(entries.includes('AI Toolkit/planner/rules.md')); assert.equal(entries.length, 4, 'ZIP should contain exactly 4 files (2 per child)'); }); - diff --git a/test/skills-package-stars.test.js b/test/skills-package-stars.test.js index 709d3e3c..120dc59a 100644 --- a/test/skills-package-stars.test.js +++ b/test/skills-package-stars.test.js @@ -88,8 +88,11 @@ test('getSkillList aggregates package stars from children (T038)', async () => { assert.equal(result.total, 1, 'List should only show parent package'); assert.equal(result.list.length, 1); assert.equal(result.list[0].slug, 'my-pack'); - assert.equal(result.list[0].stars, 35, - `Package stars should be sum of children (10 + 25 = 35), got: ${result.list[0].stars}`); + assert.equal( + result.list[0].stars, + 35, + `Package stars should be sum of children (10 + 25 = 35), got: ${result.list[0].stars}` + ); }); test('getSkillList children keep their original stars (T039)', async () => { @@ -151,8 +154,7 @@ test('getSkillList children keep their original stars (T039)', async () => { // Child should be filtered out from list assert.equal(result.total, 1); assert.equal(result.list[0].slug, 'my-pack'); - assert.equal(result.list[0].stars, 42, - 'Package stars should equal child stars (42)'); + assert.equal(result.list[0].stars, 42, 'Package stars should equal child stars (42)'); }); test('getSkillDetail aggregates package stars from children (T039 variant)', async () => { @@ -250,8 +252,11 @@ test('getSkillDetail aggregates package stars from children (T039 variant)', asy const detail = await service.getSkillDetail('test-pack'); - assert.equal(detail.stars, 20, - `Package detail stars should be sum of children (7 + 13 = 20), got: ${detail.stars}`); + assert.equal( + detail.stars, + 20, + `Package detail stars should be sum of children (7 + 13 = 20), got: ${detail.stars}` + ); assert.equal(detail.children.length, 2); assert.equal(detail.children[0].stars, 7, 'Child should keep original stars'); assert.equal(detail.children[1].stars, 13, 'Child should keep original stars'); diff --git a/test/skills-registry-contract.test.js b/test/skills-registry-contract.test.js index 5afbbe41..ebcc1e83 100644 --- a/test/skills-registry-contract.test.js +++ b/test/skills-registry-contract.test.js @@ -513,14 +513,10 @@ test('buildSkillZip packages skill package nested structure when is_package is 1 SkillsFile: { findAll: async (options) => { if (options.where.skill_id === 101) { - return [ - { file_path: 'README.md', content: 'c3ViLTE=', is_binary: 0 }, - ]; + return [{ file_path: 'README.md', content: 'c3ViLTE=', is_binary: 0 }]; } if (options.where.skill_id === 102) { - return [ - { file_path: 'index.js', content: 'c3ViLTI=', is_binary: 0 }, - ]; + return [{ file_path: 'index.js', content: 'c3ViLTI=', is_binary: 0 }]; } return []; }, @@ -535,7 +531,7 @@ test('buildSkillZip packages skill package nested structure when is_package is 1 const AdmZip = require('adm-zip'); const zip = new AdmZip(result.content); - const entries = zip.getEntries().map(e => e.entryName); + const entries = zip.getEntries().map((e) => e.entryName); assert.ok(entries.includes('my-skills-pack/sub-skill-1/README.md')); assert.ok(entries.includes('my-skills-pack/sub-skill-2/index.js')); @@ -609,9 +605,7 @@ test('publishSkill returns ok: true and string skillId and versionId', async () const result = await service.publishSkill( { slug: 'test-skill', displayName: 'Test Skill', version: '1.0.0' }, - [ - { filepath: 'SKILL.md', content: 'test content' }, - ] + [{ filepath: 'SKILL.md', content: 'test content' }] ); assert.equal(result.ok, true); From 25530d08ab3fecf6e9f05e103011840e28c729de Mon Sep 17 00:00:00 2001 From: huaiju Date: Thu, 18 Jun 2026 10:15:21 +0800 Subject: [PATCH 13/13] chore: clean up lint errors after prettier unblock With the prettier step finally green, eslint, stylelint, and the rest of the CI pipeline ran for the first time on this branch and surfaced pre-existing failures. Address them so the pipeline can be green end-to-end: - eslint --fix to apply autofixable rules across the repo - stylelint --fix to normalize hex casing in scss - ignore dt-skill/dist/ from eslint (build artifacts) - drop unused imports/vars and a redundant try/catch - rename Promise constructor params to (resolve, reject) - silence no-throw-literal where the test deliberately throws a non-Error --- .eslintignore | 2 + app/service/skills.js | 8 +- app/service/skillsRegistry.js | 1 - app/view/app.js | 2 +- app/web/components/skills/SkillCard.tsx | 1 - app/web/components/skills/style.scss | 30 +-- .../detail/components/SkillFileExplorer.tsx | 1 - app/web/pages/skills/detail/style.scss | 223 +++++++++--------- app/web/pages/skills/index.tsx | 4 +- config/config.default.js | 2 +- dt-skill/src/cli.test.ts | 2 +- dt-skill/src/cli.ts | 7 +- dt-skill/src/cli/agents.test.ts | 1 + dt-skill/src/cli/agents.ts | 1 + dt-skill/src/cli/commands/delete.test.ts | 1 + dt-skill/src/cli/commands/github.test.ts | 3 +- dt-skill/src/cli/commands/github.ts | 4 +- dt-skill/src/cli/commands/inspect.test.ts | 1 + dt-skill/src/cli/commands/inspect.ts | 10 +- dt-skill/src/cli/commands/publish.test.ts | 1 + dt-skill/src/cli/commands/publish.ts | 9 +- .../src/cli/commands/skills.install.test.ts | 4 +- dt-skill/src/cli/commands/skills.test.ts | 1 + dt-skill/src/cli/commands/skills.ts | 15 +- .../src/cli/prompts/search-multiselect.ts | 6 +- dt-skill/src/cli/registry.test.ts | 1 + dt-skill/src/cli/scanSkills.test.ts | 1 + dt-skill/src/cli/scanSkills.ts | 1 + dt-skill/src/cli/ui.ts | 9 +- dt-skill/src/config.test.ts | 1 + dt-skill/src/config.ts | 3 +- dt-skill/src/discovery.test.ts | 1 + dt-skill/src/http.bun.test.ts | 1 + dt-skill/src/http.test.ts | 2 + dt-skill/src/http.ts | 1 + dt-skill/src/schema/schemas.test.ts | 1 + .../schema/skillFingerprintContract.test.ts | 1 + dt-skill/src/schema/textFiles.test.ts | 3 +- dt-skill/src/schema/textFiles.ts | 4 +- dt-skill/src/skills.test.ts | 3 +- dt-skill/src/skills.ts | 5 +- dt-skill/test-artifact/cli.artifact.test.ts | 1 + dt-skill/test/cliCommandTestKit.ts | 1 + 43 files changed, 187 insertions(+), 193 deletions(-) diff --git a/.eslintignore b/.eslintignore index e9f080cf..83eaa25a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -16,3 +16,5 @@ app/view/ test.js config/manifest.json + +dt-skill/dist/ diff --git a/app/service/skills.js b/app/service/skills.js index f1396350..8a4b4d6e 100644 --- a/app/service/skills.js +++ b/app/service/skills.js @@ -1,11 +1,9 @@ const Service = require('egg').Service; const AdmZip = require('adm-zip'); const crypto = require('crypto'); -const { spawn } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); -const fetch = require('node-fetch'); const { createInstallKeyMap, resolveSkillIdentifier, @@ -781,7 +779,7 @@ class SkillsService extends Service { }; } - getUploadIdentityKey(skillRecords = [], preferredName = '') { + getUploadIdentityKey(_skillRecords = [], preferredName = '') { const name = String(preferredName || '').trim(); if (name) return name; return ''; @@ -2088,13 +2086,11 @@ class SkillsService extends Service { transaction, }); - let parentRow; if (globalExistingParent) { - parentRow = globalExistingParent; await globalExistingParent.update(parentPayload, { transaction }); oldRowMap.delete(parentSlug); } else { - parentRow = await SkillsItem.create(parentPayload, { transaction }); + await SkillsItem.create(parentPayload, { transaction }); } createdSkills.push({ diff --git a/app/service/skillsRegistry.js b/app/service/skillsRegistry.js index a80bfc7c..746e488f 100644 --- a/app/service/skillsRegistry.js +++ b/app/service/skillsRegistry.js @@ -1,6 +1,5 @@ const Service = require('egg').Service; const AdmZip = require('adm-zip'); -const crypto = require('crypto'); const fs = require('fs'); const ignore = require('ignore'); const path = require('path'); diff --git a/app/view/app.js b/app/view/app.js index 02ccac01..f8c073ac 100644 --- a/app/view/app.js +++ b/app/view/app.js @@ -1 +1 @@ -!function(n,t){for(var e in t)n[e]=t[e]}(exports,function(n){var t={};function e(a){if(t[a])return t[a].exports;var i=t[a]={i:a,l:!1,exports:{}};return n[a].call(i.exports,i,i.exports,e),i.l=!0,i.exports}return e.m=n,e.c=t,e.d=function(n,t,a){e.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:a})},e.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},e.t=function(n,t){if(1&t&&(n=e(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var a=Object.create(null);if(e.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var i in n)e.d(a,i,function(t){return n[t]}.bind(null,i));return a},e.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(t,"a",t),t},e.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},e.p="/public/",e(e.s=78)}([function(n,t){n.exports=require("react")},function(n,t){n.exports=require("antd")},function(n,t,e){"use strict";n.exports=function(n){var t=[];return t.toString=function(){return this.map((function(t){var e=function(n,t){var e=n[1]||"",a=n[3];if(!a)return e;if(t&&"function"==typeof btoa){var i=(o=a,l=btoa(unescape(encodeURIComponent(JSON.stringify(o)))),s="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(l),"/*# ".concat(s," */")),r=a.sources.map((function(n){return"/*# sourceURL=".concat(a.sourceRoot||"").concat(n," */")}));return[e].concat(r).concat([i]).join("\n")}var o,l,s;return[e].join("\n")}(t,n);return t[2]?"@media ".concat(t[2]," {").concat(e,"}"):e})).join("")},t.i=function(n,e,a){"string"==typeof n&&(n=[[null,n,""]]);var i={};if(a)for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:{},e=t.replace,r=void 0!==e&&e,c=t.prepend,d=void 0!==c&&c,p=[],u=0;u0&&i[i.length-1])||6!==r[0]&&2!==r[0])){o=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0?a:e)(n)}},function(n,t,e){var a=e(55)("keys"),i=e(56);n.exports=function(n){return a[n]||(a[n]=i(n))}},function(n,t){n.exports=require("js-cookie")},function(n,t,e){"use strict";var a,i;Object.defineProperty(t,"__esModule",{value:!0}),t.SUBSCRIPTIONSENDTYPECN=t.SUBSCRIPTIONSENDTYPE=t.SUBSCRIPTIONSTATUS=void 0,function(n){n[n.OPEN=1]="OPEN",n[n.CLOSE=2]="CLOSE"}(t.SUBSCRIPTIONSTATUS||(t.SUBSCRIPTIONSTATUS={})),function(n){n[n.FRIDAY=0]="FRIDAY",n[n.WORKDAY=1]="WORKDAY",n[n.EVERYDAY=2]="EVERYDAY",n[n.MONDAY=3]="MONDAY"}(i=t.SUBSCRIPTIONSENDTYPE||(t.SUBSCRIPTIONSENDTYPE={})),t.SUBSCRIPTIONSENDTYPECN=((a={})[i.MONDAY]="每周一",a[i.FRIDAY]="每周五",a[i.WORKDAY]="周一至周五",a[i.EVERYDAY]="每天",a)},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.urlReg=void 0,t.urlReg=new RegExp(/(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/,"i")},function(n,t){n.exports=require("socket.io-parser")},function(n,t,e){"use strict";n.exports=function(n,t){return t||(t={}),"string"!=typeof(n=n&&n.__esModule?n.default:n)?n:(/^['"].*['"]$/.test(n)&&(n=n.slice(1,-1)),t.hash&&(n+=t.hash),/["'() \t\n]/.test(n)||t.needQuotes?'"'.concat(n.replace(/"/g,'\\"').replace(/\n/g,"\\n"),'"'):n)}},function(n,t,e){"use strict";var a=this&&this.__awaiter||function(n,t,e,a){return new(e||(e=Promise))((function(i,r){function o(n){try{s(a.next(n))}catch(n){r(n)}}function l(n){try{s(a.throw(n))}catch(n){r(n)}}function s(n){var t;n.done?i(n.value):(t=n.value,t instanceof e?t:new e((function(n){n(t)}))).then(o,l)}s((a=a.apply(n,t||[])).next())}))},i=this&&this.__generator||function(n,t){var e,a,i,r,o={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return r={next:l(0),throw:l(1),return:l(2)},"function"==typeof Symbol&&(r[Symbol.iterator]=function(){return this}),r;function l(r){return function(l){return function(r){if(e)throw new TypeError("Generator is already executing.");for(;o;)try{if(e=1,a&&(i=2&r[0]?a.return:r[0]?a.throw||((i=a.return)&&i.call(a),0):a.next)&&!(i=i.call(a,r[1])).done)return i;switch(a=0,i&&(r=[2&r[0],i.value]),r[0]){case 0:case 1:i=r;break;case 4:return o.label++,{value:r[1],done:!1};case 5:o.label++,a=r[1],r=[0];continue;case 7:r=o.ops.pop(),o.trys.pop();continue;default:if(!(i=o.trys,(i=i.length>0&&i[i.length-1])||6!==r[0]&&2!==r[0])){o=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]=t.length?{value:void 0,done:!0}:(n=a(t,e),this._i+=n.length,{value:n,done:!1})}))},function(n,t,e){var a=e(49),i=e(9)("toStringTag"),r="Arguments"==a(function(){return arguments}());n.exports=function(n){var t,e,o;return void 0===n?"Undefined":null===n?"Null":"string"==typeof(e=function(n,t){try{return n[t]}catch(n){}}(t=Object(n),i))?e:r?a(t):"Object"==(o=a(t))&&"function"==typeof t.callee?"Arguments":o}},function(n,t,e){n.exports=e.p+"img/help-icon.3d0854a5.png"},function(n,t,e){var a=e(173),i=e(63),r=e(64),o=e(34),l=e(65),s=e(66),c=e(22)("socket.io-client:manager"),d=e(176),p=e(177),u=Object.prototype.hasOwnProperty;function b(n,t){if(!(this instanceof b))return new b(n,t);n&&"object"==typeof n&&(t=n,n=void 0),(t=t||{}).path=t.path||"/socket.io",this.nsps={},this.subs=[],this.opts=t,this.reconnection(!1!==t.reconnection),this.reconnectionAttempts(t.reconnectionAttempts||1/0),this.reconnectionDelay(t.reconnectionDelay||1e3),this.reconnectionDelayMax(t.reconnectionDelayMax||5e3),this.randomizationFactor(t.randomizationFactor||.5),this.backoff=new p({min:this.reconnectionDelay(),max:this.reconnectionDelayMax(),jitter:this.randomizationFactor()}),this.timeout(null==t.timeout?2e4:t.timeout),this.readyState="closed",this.uri=n,this.connecting=[],this.lastPing=null,this.encoding=!1,this.packetBuffer=[],this.encoder=new o.Encoder,this.decoder=new o.Decoder,this.autoConnect=!1!==t.autoConnect,this.autoConnect&&this.open()}n.exports=b,b.prototype.emitAll=function(){for(var n in this.emit.apply(this,arguments),this.nsps)u.call(this.nsps,n)&&this.nsps[n].emit.apply(this.nsps[n],arguments)},b.prototype.updateSocketIds=function(){for(var n in this.nsps)u.call(this.nsps,n)&&(this.nsps[n].id=this.engine.id)},r(b.prototype),b.prototype.reconnection=function(n){return arguments.length?(this._reconnection=!!n,this):this._reconnection},b.prototype.reconnectionAttempts=function(n){return arguments.length?(this._reconnectionAttempts=n,this):this._reconnectionAttempts},b.prototype.reconnectionDelay=function(n){return arguments.length?(this._reconnectionDelay=n,this.backoff&&this.backoff.setMin(n),this):this._reconnectionDelay},b.prototype.randomizationFactor=function(n){return arguments.length?(this._randomizationFactor=n,this.backoff&&this.backoff.setJitter(n),this):this._randomizationFactor},b.prototype.reconnectionDelayMax=function(n){return arguments.length?(this._reconnectionDelayMax=n,this.backoff&&this.backoff.setMax(n),this):this._reconnectionDelayMax},b.prototype.timeout=function(n){return arguments.length?(this._timeout=n,this):this._timeout},b.prototype.maybeReconnectOnOpen=function(){!this.reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()},b.prototype.open=b.prototype.connect=function(n,t){if(c("readyState %s",this.readyState),~this.readyState.indexOf("open"))return this;c("opening %s",this.uri),this.engine=a(this.uri,this.opts);var e=this.engine,i=this;this.readyState="opening",this.skipReconnect=!1;var r=l(e,"open",(function(){i.onopen(),n&&n()})),o=l(e,"error",(function(t){if(c("connect_error"),i.cleanup(),i.readyState="closed",i.emitAll("connect_error",t),n){var e=new Error("Connection error");e.data=t,n(e)}else i.maybeReconnectOnOpen()}));if(!1!==this._timeout){var s=this._timeout;c("connect attempt will timeout after %d",s);var d=setTimeout((function(){c("connect attempt timed out after %d",s),r.destroy(),e.close(),e.emit("error","timeout"),i.emitAll("connect_timeout",s)}),s);this.subs.push({destroy:function(){clearTimeout(d)}})}return this.subs.push(r),this.subs.push(o),this},b.prototype.onopen=function(){c("open"),this.cleanup(),this.readyState="open",this.emit("open");var n=this.engine;this.subs.push(l(n,"data",s(this,"ondata"))),this.subs.push(l(n,"ping",s(this,"onping"))),this.subs.push(l(n,"pong",s(this,"onpong"))),this.subs.push(l(n,"error",s(this,"onerror"))),this.subs.push(l(n,"close",s(this,"onclose"))),this.subs.push(l(this.decoder,"decoded",s(this,"ondecoded")))},b.prototype.onping=function(){this.lastPing=new Date,this.emitAll("ping")},b.prototype.onpong=function(){this.emitAll("pong",new Date-this.lastPing)},b.prototype.ondata=function(n){this.decoder.add(n)},b.prototype.ondecoded=function(n){this.emit("packet",n)},b.prototype.onerror=function(n){c("error",n),this.emitAll("error",n)},b.prototype.socket=function(n,t){var e=this.nsps[n];if(!e){e=new i(this,n,t),this.nsps[n]=e;var a=this;e.on("connecting",r),e.on("connect",(function(){e.id=a.engine.id})),this.autoConnect&&r()}function r(){~d(a.connecting,e)||a.connecting.push(e)}return e},b.prototype.destroy=function(n){var t=d(this.connecting,n);~t&&this.connecting.splice(t,1),this.connecting.length||this.close()},b.prototype.packet=function(n){c("writing packet %j",n);var t=this;n.query&&0===n.type&&(n.nsp+="?"+n.query),t.encoding?t.packetBuffer.push(n):(t.encoding=!0,this.encoder.encode(n,(function(e){for(var a=0;a0&&!this.encoding){var n=this.packetBuffer.shift();this.packet(n)}},b.prototype.cleanup=function(){c("cleanup");for(var n=this.subs.length,t=0;t=this._reconnectionAttempts)c("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var t=this.backoff.duration();c("will wait %dms before reconnect attempt",t),this.reconnecting=!0;var e=setTimeout((function(){n.skipReconnect||(c("attempting reconnect"),n.emitAll("reconnect_attempt",n.backoff.attempts),n.emitAll("reconnecting",n.backoff.attempts),n.skipReconnect||n.open((function(t){t?(c("reconnect attempt error"),n.reconnecting=!1,n.reconnect(),n.emitAll("reconnect_error",t.data)):(c("reconnect success"),n.onreconnect())})))}),t);this.subs.push({destroy:function(){clearTimeout(e)}})}},b.prototype.onreconnect=function(){var n=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",n)}},function(n,t,e){var a=e(34),i=e(64),r=e(174),o=e(65),l=e(66),s=e(22)("socket.io-client:socket"),c=e(175);n.exports=u;var d={connect:1,connect_error:1,connect_timeout:1,connecting:1,disconnect:1,error:1,reconnect:1,reconnect_attempt:1,reconnect_failed:1,reconnect_error:1,reconnecting:1,ping:1,pong:1},p=i.prototype.emit;function u(n,t,e){this.io=n,this.nsp=t,this.json=this,this.ids=0,this.acks={},this.receiveBuffer=[],this.sendBuffer=[],this.connected=!1,this.disconnected=!0,e&&e.query&&(this.query=e.query),this.io.autoConnect&&this.open()}i(u.prototype),u.prototype.subEvents=function(){if(!this.subs){var n=this.io;this.subs=[o(n,"open",l(this,"onopen")),o(n,"packet",l(this,"onpacket")),o(n,"close",l(this,"onclose"))]}},u.prototype.open=u.prototype.connect=function(){return this.connected||(this.subEvents(),this.io.open(),"open"===this.io.readyState&&this.onopen(),this.emit("connecting")),this},u.prototype.send=function(){var n=r(arguments);return n.unshift("message"),this.emit.apply(this,n),this},u.prototype.emit=function(n){if(d.hasOwnProperty(n))return p.apply(this,arguments),this;var t=r(arguments),e=a.EVENT;c(t)&&(e=a.BINARY_EVENT);var i={type:e,data:t,options:{}};return i.options.compress=!this.flags||!1!==this.flags.compress,"function"==typeof t[t.length-1]&&(s("emitting packet with ack id %d",this.ids),this.acks[this.ids]=t.pop(),i.id=this.ids++),this.connected?this.packet(i):this.sendBuffer.push(i),delete this.flags,this},u.prototype.packet=function(n){n.nsp=this.nsp,this.io.packet(n)},u.prototype.onopen=function(){s("transport is open - connecting"),"/"!==this.nsp&&(this.query?this.packet({type:a.CONNECT,query:this.query}):this.packet({type:a.CONNECT}))},u.prototype.onclose=function(n){s("close (%s)",n),this.connected=!1,this.disconnected=!0,delete this.id,this.emit("disconnect",n)},u.prototype.onpacket=function(n){if(n.nsp===this.nsp)switch(n.type){case a.CONNECT:this.onconnect();break;case a.EVENT:case a.BINARY_EVENT:this.onevent(n);break;case a.ACK:case a.BINARY_ACK:this.onack(n);break;case a.DISCONNECT:this.ondisconnect();break;case a.ERROR:this.emit("error",n.data)}},u.prototype.onevent=function(n){var t=n.data||[];s("emitting event %j",t),null!=n.id&&(s("attaching ack callback to event"),t.push(this.ack(n.id))),this.connected?p.apply(this,t):this.receiveBuffer.push(t)},u.prototype.ack=function(n){var t=this,e=!1;return function(){if(!e){e=!0;var i=r(arguments);s("sending ack %j",i);var o=c(i)?a.BINARY_ACK:a.ACK;t.packet({type:o,id:n,data:i})}}},u.prototype.onack=function(n){var t=this.acks[n.id];"function"==typeof t?(s("calling ack %s with %j",n.id,n.data),t.apply(this,n.data),delete this.acks[n.id]):s("bad ack %s",n.id)},u.prototype.onconnect=function(){this.connected=!0,this.disconnected=!1,this.emit("connect"),this.emitBuffered()},u.prototype.emitBuffered=function(){var n;for(n=0;n0&&c.forEach((function(n){n.key&&n.value&&(u[n.key]=n.value)}));var b={mcpServers:(t={},t[r||"your-server-name"]={command:l||"node",args:s?(i=s,i.split(/[\s\n]+/).map((function(n){return n.trim()})).filter((function(n){return n.length>0}))):["path/to/your/server.js"],env:Object.keys(u).length>0?u:void 0},t)};return JSON.stringify(b,null,2)}return"streamable-http"===o?JSON.stringify({mcpServers:(e={},e[r||"your-server-name"]={url:d},e)},null,2):"sse"===o?JSON.stringify({mcpServers:(a={},a[r||"your-server-name"]={url:p},a)},null,2):"{}"}},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var i=Object.getOwnPropertyDescriptor(t,e);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,i)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),i=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),r=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return i(t,n),t},o=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=r(e(0)),s=o(e(8)),c=s.default.mcpInspectorWebPort,d=s.default.mcpInspectorServerPort;t.default=function(n){var t=n.config,e=(0,l.useMemo)((function(){return function(n){var t;if("undefined"==typeof location)return"";var e={MCP_PROXY_PORT:d.toString(),transport:(null==n?void 0:n.transport)||"streamable-http",serverCommand:(null==n?void 0:n.serverCommand)||"npx",serverArgs:(null===(t=null==n?void 0:n.serverArgs)||void 0===t?void 0:t.join(","))||"",serverUrl:(null==n?void 0:n.serverUrl)||"".concat(location.protocol,"//").concat(location.hostname,":").concat(s.default.mcpEndpointPort,"/mcp-endpoint/your-server-id/mcp")};return new URLSearchParams(e).toString()}(t)}),[t]);return"undefined"==typeof location?null:l.default.createElement("div",{className:"mcp-inspector-iframe",style:{minHeight:800,height:800}},l.default.createElement("iframe",{src:"".concat(location.protocol,"//").concat(location.hostname,":").concat(c,"?").concat(e),style:{width:"100%",height:"100%"}}))}},function(n,t){n.exports=require("react-syntax-highlighter")},function(n,t){n.exports=require("react-syntax-highlighter/dist/cjs/styles/hljs")},function(n,t,e){"use strict";var a=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0}),t.SkillCard=void 0;var i=a(e(0)),r=e(1),o=e(4);t.SkillCard=function(n){var t,e=n.skill,a=n.onClick,l=n.onEdit,s=n.showMeta,c=void 0===s||s,d=n.showChildrenPreview,p=void 0!==d&&d,u=n.childrenList,b=void 0===u?[]:u,f=n.size,m=void 0===f?"default":f,g=n.selected,h=void 0!==g&&g,x=n.onSelect,w=1===e.isPackage,k="compact"===m;return i.default.createElement(r.Card,{className:"skill-card-unified ".concat(w?"is-package":""," ").concat(k?"is-compact":""," ").concat(h?"is-selected":""),hoverable:!0,onClick:function(){return a(e)}},i.default.createElement("div",{className:"card-main"},i.default.createElement("div",{className:"card-icon-shell"},w?i.default.createElement(o.FolderOutlined,{className:"card-icon is-package-icon"}):i.default.createElement(o.FileTextOutlined,{className:"card-icon is-skill-icon"})),i.default.createElement("div",{className:"card-content"},i.default.createElement("div",{className:"card-header-row"},i.default.createElement("span",{className:"card-name"},e.name),i.default.createElement("div",{className:"card-badges"},x&&i.default.createElement(r.Checkbox,{className:"select-checkbox",checked:h,onClick:function(n){return n.stopPropagation()},onChange:function(n){return x(e,n.target.checked)}}),w&&i.default.createElement(r.Tag,{color:"magenta",className:"package-badge"},"技能包"),i.default.createElement("span",{className:"stars-badge"},i.default.createElement(o.StarOutlined,null)," ",e.stars||0),l&&i.default.createElement("button",{type:"button",className:"edit-trigger",onClick:function(n){n.stopPropagation(),l(e)},"aria-label":"编辑 ".concat(e.name)},i.default.createElement("svg",{width:"14",height:"14",viewBox:"0 0 14 14",fill:"none"},i.default.createElement("path",{d:"M9.916 2.334a1.65 1.65 0 1 1 2.334 2.332l-6.27 6.27a1.5 1.5 0 0 1-.707.39l-2.024.45.45-2.025a1.5 1.5 0 0 1 .39-.706l6.27-6.27Z",stroke:"currentColor",strokeWidth:"1.2",strokeLinecap:"round",strokeLinejoin:"round"}))))),i.default.createElement("p",{className:"card-description"},e.description||"暂无描述"),i.default.createElement("div",{className:"card-tags"},i.default.createElement(r.Tag,{className:"category-tag"},e.category||"通用"),null===(t=e.tags)||void 0===t?void 0:t.slice(0,3).map((function(n){return i.default.createElement(r.Tag,{key:n,className:"skill-tag"},n)}))),p&&w&&b.length>0&&i.default.createElement("div",{className:"children-preview"},i.default.createElement("span",{className:"children-label"},"包含:"),i.default.createElement("span",{className:"children-names"},b.slice(0,3).map((function(n){return n.name})).join(" · "),b.length>3&&" 等 ".concat(b.length," 个"))))),c&&i.default.createElement("div",{className:"card-meta"},i.default.createElement("span",{className:"meta-item"},i.default.createElement("span",{className:"meta-label"},"来源"),i.default.createElement("span",{className:"meta-value",title:e.sourceRepo||e.sourcePath||"-"},e.sourceRepo||e.sourcePath||"-")),i.default.createElement("span",{className:"meta-separator"},"·"),i.default.createElement("span",{className:"meta-item"},i.default.createElement("span",{className:"meta-label"},"更新"),i.default.createElement("span",{className:"meta-value"},e.updatedAt?new Date(e.updatedAt).toLocaleDateString("zh-CN"):"-"))))}},function(n,t){n.exports=require("react-codemirror2")},function(n,t,e){"use strict";var a=this&&this.__assign||function(){return(a=Object.assign||function(n){for(var t,e=1,a=arguments.length;e*\/]/.test(p)?l(null,"select-op"):/[;{}:\[\]]/.test(p)?l(null,p):(n.eatWhile(/[\w\\\-]/),l("variable","variable")):l(null,"compare"):void l(null,"compare")}function c(n,t){for(var e,a=!1;null!=(e=n.next());){if(a&&"/"==e){t.tokenize=s;break}a="*"==e}return l("comment","comment")}function d(n,t){for(var e,a=0;null!=(e=n.next());){if(a>=2&&">"==e){t.tokenize=s;break}a="-"==e?a+1:0}return l("comment","comment")}return{startState:function(n){return{tokenize:s,baseIndent:n||0,stack:[]}},token:function(n,t){if(n.eatSpace())return null;e=null;var a=t.tokenize(n,t),i=t.stack[t.stack.length-1];return"hash"==e&&"rule"==i?a="atom":"variable"==a&&("rule"==i?a="number":i&&"@media{"!=i||(a="tag")),"rule"==i&&/^[\{\};]$/.test(e)&&t.stack.pop(),"{"==e?"@media"==i?t.stack[t.stack.length-1]="@media{":t.stack.push("{"):"}"==e?t.stack.pop():"@media"==e?t.stack.push("@media"):"{"==i&&"comment"!=e&&t.stack.push("rule"),a},indent:function(n,t){var e=n.stack.length;return/^\}/.test(t)&&(e-="rule"==n.stack[n.stack.length-1]?2:1),n.baseIndent+e*o},electricChars:"}"}})),n.defineMIME("text/x-nginx-conf","nginx")}(e(75))},function(n,t,e){n.exports=function(){"use strict";var n=navigator.userAgent,t=navigator.platform,e=/gecko\/\d/i.test(n),a=/MSIE \d/.test(n),i=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(n),r=/Edge\/(\d+)/.exec(n),o=a||i||r,l=o&&(a?document.documentMode||6:+(r||i)[1]),s=!r&&/WebKit\//.test(n),c=s&&/Qt\/\d+\.\d+/.test(n),d=!r&&/Chrome\/(\d+)/.exec(n),p=d&&+d[1],u=/Opera\//.test(n),b=/Apple Computer/.test(navigator.vendor),f=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(n),m=/PhantomJS/.test(n),g=b&&(/Mobile\/\w+/.test(n)||navigator.maxTouchPoints>2),h=/Android/.test(n),x=g||h||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(n),w=g||/Mac/.test(t),k=/\bCrOS\b/.test(n),y=/win/i.test(t),v=u&&n.match(/Version\/(\d*\.\d*)/);v&&(v=Number(v[1])),v&&v>=15&&(u=!1,s=!0);var z=w&&(c||u&&(null==v||v<12.11)),E=e||o&&l>=9;function _(n){return new RegExp("(^|\\s)"+n+"(?:$|\\s)\\s*")}var C,F=function(n,t){var e=n.className,a=_(t).exec(e);if(a){var i=e.slice(a.index+a[0].length);n.className=e.slice(0,a.index)+(i?a[1]+i:"")}};function M(n){for(var t=n.childNodes.length;t>0;--t)n.removeChild(n.firstChild);return n}function S(n,t){return M(n).appendChild(t)}function D(n,t,e,a){var i=document.createElement(n);if(e&&(i.className=e),a&&(i.style.cssText=a),"string"==typeof t)i.appendChild(document.createTextNode(t));else if(t)for(var r=0;r=t)return o+(t-r);o+=l-r,o+=e-o%e,r=l+1}}g?T=function(n){n.selectionStart=0,n.selectionEnd=n.value.length}:o&&(T=function(n){try{n.select()}catch(n){}});var U=function(){this.id=null,this.f=null,this.time=0,this.handler=B(this.onTimeout,this)};function H(n,t){for(var e=0;e=t)return a+Math.min(o,t-i);if(i+=r-a,a=r+1,(i+=e-i%e)>=t)return a}}var G=[""];function Q(n){for(;G.length<=n;)G.push(K(G)+" ");return G[n]}function K(n){return n[n.length-1]}function J(n,t){for(var e=[],a=0;a"€"&&(n.toUpperCase()!=n.toLowerCase()||tn.test(n))}function an(n,t){return t?!!(t.source.indexOf("\\w")>-1&&en(n))||t.test(n):en(n)}function rn(n){for(var t in n)if(n.hasOwnProperty(t)&&n[t])return!1;return!0}var on=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function ln(n){return n.charCodeAt(0)>=768&&on.test(n)}function sn(n,t,e){for(;(e<0?t>0:te?-1:1;;){if(t==e)return t;var i=(t+e)/2,r=a<0?Math.ceil(i):Math.floor(i);if(r==t)return n(r)?t:e;n(r)?e=r:t=r+a}}var dn=null;function pn(n,t,e){var a;dn=null;for(var i=0;it)return i;r.to==t&&(r.from!=r.to&&"before"==e?a=i:dn=i),r.from==t&&(r.from!=r.to&&"before"!=e?a=i:dn=i)}return null!=a?a:dn}var un=function(){var n=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,t=/[stwN]/,e=/[LRr]/,a=/[Lb1n]/,i=/[1n]/;function r(n,t,e){this.level=n,this.from=t,this.to=e}return function(o,l){var s="ltr"==l?"L":"R";if(0==o.length||"ltr"==l&&!n.test(o))return!1;for(var c,d=o.length,p=[],u=0;u-1&&(a[t]=i.slice(0,r).concat(i.slice(r+1)))}}}function xn(n,t){var e=gn(n,t);if(e.length)for(var a=Array.prototype.slice.call(arguments,2),i=0;i0}function vn(n){n.prototype.on=function(n,t){mn(this,n,t)},n.prototype.off=function(n,t){hn(this,n,t)}}function zn(n){n.preventDefault?n.preventDefault():n.returnValue=!1}function En(n){n.stopPropagation?n.stopPropagation():n.cancelBubble=!0}function _n(n){return null!=n.defaultPrevented?n.defaultPrevented:0==n.returnValue}function Cn(n){zn(n),En(n)}function Fn(n){return n.target||n.srcElement}function Mn(n){var t=n.which;return null==t&&(1&n.button?t=1:2&n.button?t=3:4&n.button&&(t=2)),w&&n.ctrlKey&&1==t&&(t=3),t}var Sn,Dn,jn=function(){if(o&&l<9)return!1;var n=D("div");return"draggable"in n||"dragDrop"in n}();function On(n){if(null==Sn){var t=D("span","​");S(n,D("span",[t,document.createTextNode("x")])),0!=n.firstChild.offsetHeight&&(Sn=t.offsetWidth<=1&&t.offsetHeight>2&&!(o&&l<8))}var e=Sn?D("span","​"):D("span"," ",null,"display: inline-block; width: 1px; margin-right: -1px");return e.setAttribute("cm-text",""),e}function In(n){if(null!=Dn)return Dn;var t=S(n,document.createTextNode("AخA")),e=C(t,0,1).getBoundingClientRect(),a=C(t,1,2).getBoundingClientRect();return M(n),!(!e||e.left==e.right)&&(Dn=a.right-e.right<3)}var Nn,An=3!="\n\nb".split(/\n/).length?function(n){for(var t=0,e=[],a=n.length;t<=a;){var i=n.indexOf("\n",t);-1==i&&(i=n.length);var r=n.slice(t,"\r"==n.charAt(i-1)?i-1:i),o=r.indexOf("\r");-1!=o?(e.push(r.slice(0,o)),t+=o+1):(e.push(r),t=i+1)}return e}:function(n){return n.split(/\r\n?|\n/)},Tn=window.getSelection?function(n){try{return n.selectionStart!=n.selectionEnd}catch(n){return!1}}:function(n){var t;try{t=n.ownerDocument.selection.createRange()}catch(n){}return!(!t||t.parentElement()!=n)&&0!=t.compareEndPoints("StartToEnd",t)},Ln="oncopy"in(Nn=D("div"))||(Nn.setAttribute("oncopy","return;"),"function"==typeof Nn.oncopy),Pn=null,Bn={},Rn={};function Yn(n,t){arguments.length>2&&(t.dependencies=Array.prototype.slice.call(arguments,2)),Bn[n]=t}function Un(n){if("string"==typeof n&&Rn.hasOwnProperty(n))n=Rn[n];else if(n&&"string"==typeof n.name&&Rn.hasOwnProperty(n.name)){var t=Rn[n.name];"string"==typeof t&&(t={name:t}),(n=nn(t,n)).name=t.name}else{if("string"==typeof n&&/^[\w\-]+\/[\w\-]+\+xml$/.test(n))return Un("application/xml");if("string"==typeof n&&/^[\w\-]+\/[\w\-]+\+json$/.test(n))return Un("application/json")}return"string"==typeof n?{name:n}:n||{name:"null"}}function Hn(n,t){t=Un(t);var e=Bn[t.name];if(!e)return Hn(n,"text/plain");var a=e(n,t);if(Wn.hasOwnProperty(t.name)){var i=Wn[t.name];for(var r in i)i.hasOwnProperty(r)&&(a.hasOwnProperty(r)&&(a["_"+r]=a[r]),a[r]=i[r])}if(a.name=t.name,t.helperType&&(a.helperType=t.helperType),t.modeProps)for(var o in t.modeProps)a[o]=t.modeProps[o];return a}var Wn={};function Zn(n,t){R(t,Wn.hasOwnProperty(n)?Wn[n]:Wn[n]={})}function Vn(n,t){if(!0===t)return t;if(n.copyState)return n.copyState(t);var e={};for(var a in t){var i=t[a];i instanceof Array&&(i=i.concat([])),e[a]=i}return e}function Xn(n,t){for(var e;n.innerMode&&(e=n.innerMode(t))&&e.mode!=n;)t=e.state,n=e.mode;return e||{mode:n,state:t}}function qn(n,t,e){return!n.startState||n.startState(t,e)}var Gn=function(n,t,e){this.pos=this.start=0,this.string=n,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=e};function Qn(n,t){if((t-=n.first)<0||t>=n.size)throw new Error("There is no line "+(t+n.first)+" in the document.");for(var e=n;!e.lines;)for(var a=0;;++a){var i=e.children[a],r=i.chunkSize();if(t=n.first&&te?it(e,Qn(n,e).text.length):function(n,t){var e=n.ch;return null==e||e>t?it(n.line,t):e<0?it(n.line,0):n}(t,Qn(n,t.line).text.length)}function ut(n,t){for(var e=[],a=0;a=this.string.length},Gn.prototype.sol=function(){return this.pos==this.lineStart},Gn.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},Gn.prototype.next=function(){if(this.post},Gn.prototype.eatSpace=function(){for(var n=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>n},Gn.prototype.skipToEnd=function(){this.pos=this.string.length},Gn.prototype.skipTo=function(n){var t=this.string.indexOf(n,this.pos);if(t>-1)return this.pos=t,!0},Gn.prototype.backUp=function(n){this.pos-=n},Gn.prototype.column=function(){return this.lastColumnPos0?null:(a&&!1!==t&&(this.pos+=a[0].length),a)}var i=function(n){return e?n.toLowerCase():n};if(i(this.string.substr(this.pos,n.length))==i(n))return!1!==t&&(this.pos+=n.length),!0},Gn.prototype.current=function(){return this.string.slice(this.start,this.pos)},Gn.prototype.hideFirstChars=function(n,t){this.lineStart+=n;try{return t()}finally{this.lineStart-=n}},Gn.prototype.lookAhead=function(n){var t=this.lineOracle;return t&&t.lookAhead(n)},Gn.prototype.baseToken=function(){var n=this.lineOracle;return n&&n.baseToken(this.pos)};var bt=function(n,t){this.state=n,this.lookAhead=t},ft=function(n,t,e,a){this.state=t,this.doc=n,this.line=e,this.maxLookAhead=a||0,this.baseTokens=null,this.baseTokenPos=1};function mt(n,t,e,a){var i=[n.state.modeGen],r={};Et(n,t.text,n.doc.mode,e,(function(n,t){return i.push(n,t)}),r,a);for(var o=e.state,l=function(a){e.baseTokens=i;var l=n.state.overlays[a],s=1,c=0;e.state=!0,Et(n,t.text,l.mode,e,(function(n,t){for(var e=s;cn&&i.splice(s,1,n,i[s+1],a),s+=2,c=Math.min(n,a)}if(t)if(l.opaque)i.splice(e,s-e,n,"overlay "+t),s=e+2;else for(;en.options.maxHighlightLength&&Vn(n.doc.mode,a.state),r=mt(n,t,a);i&&(a.state=i),t.stateAfter=a.save(!i),t.styles=r.styles,r.classes?t.styleClasses=r.classes:t.styleClasses&&(t.styleClasses=null),e===n.doc.highlightFrontier&&(n.doc.modeFrontier=Math.max(n.doc.modeFrontier,++n.doc.highlightFrontier))}return t.styles}function ht(n,t,e){var a=n.doc,i=n.display;if(!a.mode.startState)return new ft(a,!0,t);var r=function(n,t,e){for(var a,i,r=n.doc,o=e?-1:t-(n.doc.mode.innerMode?1e3:100),l=t;l>o;--l){if(l<=r.first)return r.first;var s=Qn(r,l-1),c=s.stateAfter;if(c&&(!e||l+(c instanceof bt?c.lookAhead:0)<=r.modeFrontier))return l;var d=Y(s.text,null,n.options.tabSize);(null==i||a>d)&&(i=l-1,a=d)}return i}(n,t,e),o=r>a.first&&Qn(a,r-1).stateAfter,l=o?ft.fromSaved(a,o,r):new ft(a,qn(a.mode),r);return a.iter(r,t,(function(e){xt(n,e.text,l);var a=l.line;e.stateAfter=a==t-1||a%5==0||a>=i.viewFrom&&at.start)return r}throw new Error("Mode "+n.name+" failed to advance stream.")}ft.prototype.lookAhead=function(n){var t=this.doc.getLine(this.line+n);return null!=t&&n>this.maxLookAhead&&(this.maxLookAhead=n),t},ft.prototype.baseToken=function(n){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=n;)this.baseTokenPos+=2;var t=this.baseTokens[this.baseTokenPos+1];return{type:t&&t.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-n}},ft.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},ft.fromSaved=function(n,t,e){return t instanceof bt?new ft(n,Vn(n.mode,t.state),e,t.lookAhead):new ft(n,Vn(n.mode,t),e)},ft.prototype.save=function(n){var t=!1!==n?Vn(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new bt(t,this.maxLookAhead):t};var yt=function(n,t,e){this.start=n.start,this.end=n.pos,this.string=n.current(),this.type=t||null,this.state=e};function vt(n,t,e,a){var i,r,o=n.doc,l=o.mode,s=Qn(o,(t=pt(o,t)).line),c=ht(n,t.line,e),d=new Gn(s.text,n.options.tabSize,c);for(a&&(r=[]);(a||d.posn.options.maxHighlightLength?(l=!1,o&&xt(n,t,a,p.pos),p.pos=t.length,s=null):s=zt(kt(e,p,a.state,u),r),u){var b=u[0].name;b&&(s="m-"+(s?b+" "+s:b))}if(!l||d!=s){for(;c=t:r.to>t);(a||(a=[])).push(new Ft(o,r.from,l?null:r.to))}}return a}(e,i,o),s=function(n,t,e){var a;if(n)for(var i=0;i=t:r.to>t)||r.from==t&&"bookmark"==o.type&&(!e||r.marker.insertLeft)){var l=null==r.from||(o.inclusiveLeft?r.from<=t:r.from0&&l)for(var w=0;wt)&&(!e||Tt(e,r.marker)<0)&&(e=r.marker)}return e}function Yt(n,t,e,a,i){var r=Qn(n,t),o=Ct&&r.markedSpans;if(o)for(var l=0;l=0&&p<=0||d<=0&&p>=0)&&(d<=0&&(s.marker.inclusiveRight&&i.inclusiveLeft?rt(c.to,e)>=0:rt(c.to,e)>0)||d>=0&&(s.marker.inclusiveRight&&i.inclusiveLeft?rt(c.from,a)<=0:rt(c.from,a)<0)))return!0}}}function Ut(n){for(var t;t=Pt(n);)n=t.find(-1,!0).line;return n}function Ht(n,t){var e=Qn(n,t),a=Ut(e);return e==a?t:nt(a)}function Wt(n,t){if(t>n.lastLine())return t;var e,a=Qn(n,t);if(!Zt(n,a))return t;for(;e=Bt(a);)a=e.find(1,!0).line;return nt(a)+1}function Zt(n,t){var e=Ct&&t.markedSpans;if(e)for(var a=void 0,i=0;it.maxLineLength&&(t.maxLineLength=e,t.maxLine=n)}))}var Qt=function(n,t,e){this.text=n,It(this,t),this.height=e?e(this):1};function Kt(n){n.parent=null,Ot(n)}Qt.prototype.lineNo=function(){return nt(this)},vn(Qt);var Jt={},$t={};function ne(n,t){if(!n||/^\s*$/.test(n))return null;var e=t.addModeClass?$t:Jt;return e[n]||(e[n]=n.replace(/\S+/g,"cm-$&"))}function te(n,t){var e=j("span",null,null,s?"padding-right: .1px":null),a={pre:j("pre",[e],"CodeMirror-line"),content:e,col:0,pos:0,cm:n,trailingSpace:!1,splitSpaces:n.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var r=i?t.rest[i-1]:t.line,o=void 0;a.pos=0,a.addToken=ae,In(n.display.measure)&&(o=bn(r,n.doc.direction))&&(a.addToken=ie(a.addToken,o)),a.map=[],oe(r,a,gt(n,r,t!=n.display.externalMeasured&&nt(r))),r.styleClasses&&(r.styleClasses.bgClass&&(a.bgClass=A(r.styleClasses.bgClass,a.bgClass||"")),r.styleClasses.textClass&&(a.textClass=A(r.styleClasses.textClass,a.textClass||""))),0==a.map.length&&a.map.push(0,0,a.content.appendChild(On(n.display.measure))),0==i?(t.measure.map=a.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(a.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(s){var l=a.content.lastChild;(/\bcm-tab\b/.test(l.className)||l.querySelector&&l.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return xn(n,"renderLine",n,t.line,a.pre),a.pre.className&&(a.textClass=A(a.pre.className,a.textClass||"")),a}function ee(n){var t=D("span","•","cm-invalidchar");return t.title="\\u"+n.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function ae(n,t,e,a,i,r,s){if(t){var c,d=n.splitSpaces?function(n,t){if(n.length>1&&!/ /.test(n))return n;for(var e=t,a="",i=0;ic&&p.from<=c);u++);if(p.to>=d)return n(e,a,i,r,o,l,s);n(e,a.slice(0,p.to-c),i,r,null,l,s),r=null,a=a.slice(p.to-c),c=p.to}}}function re(n,t,e,a){var i=!a&&e.widgetNode;i&&n.map.push(n.pos,n.pos+t,i),!a&&n.cm.display.input.needsContentAttribute&&(i||(i=n.content.appendChild(document.createElement("span"))),i.setAttribute("cm-marker",e.id)),i&&(n.cm.display.input.setUneditable(i),n.content.appendChild(i)),n.pos+=t,n.trailingSpace=!1}function oe(n,t,e){var a=n.markedSpans,i=n.text,r=0;if(a)for(var o,l,s,c,d,p,u,b=i.length,f=0,m=1,g="",h=0;;){if(h==f){s=c=d=l="",u=null,p=null,h=1/0;for(var x=[],w=void 0,k=0;kf||v.collapsed&&y.to==f&&y.from==f)){if(null!=y.to&&y.to!=f&&h>y.to&&(h=y.to,c=""),v.className&&(s+=" "+v.className),v.css&&(l=(l?l+";":"")+v.css),v.startStyle&&y.from==f&&(d+=" "+v.startStyle),v.endStyle&&y.to==h&&(w||(w=[])).push(v.endStyle,y.to),v.title&&((u||(u={})).title=v.title),v.attributes)for(var z in v.attributes)(u||(u={}))[z]=v.attributes[z];v.collapsed&&(!p||Tt(p.marker,v)<0)&&(p=y)}else y.from>f&&h>y.from&&(h=y.from)}if(w)for(var E=0;E=b)break;for(var C=Math.min(b,h);;){if(g){var F=f+g.length;if(!p){var M=F>C?g.slice(0,C-f):g;t.addToken(t,M,o?o+s:s,d,f+M.length==h?c:"",l,u)}if(F>=C){g=g.slice(C-f),f=C;break}f=F,d=""}g=i.slice(r,r=e[m++]),o=ne(e[m++],t.cm.options)}}else for(var S=1;Se)return{map:n.measure.maps[i],cache:n.measure.caches[i],before:!0}}}function Ie(n,t,e,a){return Te(n,Ae(n,t),e,a)}function Ne(n,t){if(t>=n.display.viewFrom&&t=e.lineN&&t2&&r.push((s.bottom+c.top)/2-e.top)}}r.push(e.bottom-e.top)}}(n,t.view,t.rect),t.hasHeights=!0),(r=function(n,t,e,a){var i,r=Be(t.map,e,a),s=r.node,c=r.start,d=r.end,p=r.collapse;if(3==s.nodeType){for(var u=0;u<4;u++){for(;c&&ln(t.line.text.charAt(r.coverStart+c));)--c;for(;r.coverStart+d1}(n))return t;var e=screen.logicalXDPI/screen.deviceXDPI,a=screen.logicalYDPI/screen.deviceYDPI;return{left:t.left*e,right:t.right*e,top:t.top*a,bottom:t.bottom*a}}(n.display.measure,i))}else{var b;c>0&&(p=a="right"),i=n.options.lineWrapping&&(b=s.getClientRects()).length>1?b["right"==a?b.length-1:0]:s.getBoundingClientRect()}if(o&&l<9&&!c&&(!i||!i.left&&!i.right)){var f=s.parentNode.getClientRects()[0];i=f?{left:f.left,right:f.left+la(n.display),top:f.top,bottom:f.bottom}:Pe}for(var m=i.top-t.rect.top,g=i.bottom-t.rect.top,h=(m+g)/2,x=t.view.measure.heights,w=0;wt)&&(i=(r=s-l)-1,t>=s&&(o="right")),null!=i){if(a=n[c+2],l==s&&e==(a.insertLeft?"left":"right")&&(o=e),"left"==e&&0==i)for(;c&&n[c-2]==n[c-3]&&n[c-1].insertLeft;)a=n[2+(c-=3)],o="left";if("right"==e&&i==s-l)for(;c=0&&(e=n[i]).left==e.right;i--);return e}function Ye(n){if(n.measure&&(n.measure.cache={},n.measure.heights=null,n.rest))for(var t=0;t=a.text.length?(s=a.text.length,c="before"):s<=0&&(s=0,c="after"),!l)return o("before"==c?s-1:s,"before"==c);function d(n,t,e){return o(e?n-1:n,1==l[t].level!=e)}var p=pn(l,s,c),u=dn,b=d(s,p,"before"==c);return null!=u&&(b.other=d(s,u,"before"!=c)),b}function Ke(n,t){var e=0;t=pt(n.doc,t),n.options.lineWrapping||(e=la(n.display)*t.ch);var a=Qn(n.doc,t.line),i=Xt(a)+Ce(n.display);return{left:e,right:e,top:i,bottom:i+a.height}}function Je(n,t,e,a,i){var r=it(n,t,e);return r.xRel=i,a&&(r.outside=a),r}function $e(n,t,e){var a=n.doc;if((e+=n.display.viewOffset)<0)return Je(a.first,0,null,-1,-1);var i=tt(a,e),r=a.first+a.size-1;if(i>r)return Je(a.first+a.size-1,Qn(a,r).text.length,null,1,1);t<0&&(t=0);for(var o=Qn(a,i);;){var l=aa(n,o,i,t,e),s=Rt(o,l.ch+(l.xRel>0||l.outside>0?1:0));if(!s)return l;var c=s.find(1);if(c.line==i)return c;o=Qn(a,i=c.line)}}function na(n,t,e,a){a-=Ve(t);var i=t.text.length,r=cn((function(t){return Te(n,e,t-1).bottom<=a}),i,0);return{begin:r,end:i=cn((function(t){return Te(n,e,t).top>a}),r,i)}}function ta(n,t,e,a){return e||(e=Ae(n,t)),na(n,t,e,Xe(n,t,Te(n,e,a),"line").top)}function ea(n,t,e,a){return!(n.bottom<=e)&&(n.top>e||(a?n.left:n.right)>t)}function aa(n,t,e,a,i){i-=Xt(t);var r=Ae(n,t),o=Ve(t),l=0,s=t.text.length,c=!0,d=bn(t,n.doc.direction);if(d){var p=(n.options.lineWrapping?ra:ia)(n,t,e,r,d,a,i);l=(c=1!=p.level)?p.from:p.to-1,s=c?p.to:p.from-1}var u,b,f=null,m=null,g=cn((function(t){var e=Te(n,r,t);return e.top+=o,e.bottom+=o,!!ea(e,a,i,!1)&&(e.top<=i&&e.left<=a&&(f=t,m=e),!0)}),l,s),h=!1;if(m){var x=a-m.left=k.bottom?1:0}return Je(e,g=sn(t.text,g,1),b,h,a-u)}function ia(n,t,e,a,i,r,o){var l=cn((function(l){var s=i[l],c=1!=s.level;return ea(Qe(n,it(e,c?s.to:s.from,c?"before":"after"),"line",t,a),r,o,!0)}),0,i.length-1),s=i[l];if(l>0){var c=1!=s.level,d=Qe(n,it(e,c?s.from:s.to,c?"after":"before"),"line",t,a);ea(d,r,o,!0)&&d.top>o&&(s=i[l-1])}return s}function ra(n,t,e,a,i,r,o){var l=na(n,t,a,o),s=l.begin,c=l.end;/\s/.test(t.text.charAt(c-1))&&c--;for(var d=null,p=null,u=0;u=c||b.to<=s)){var f=Te(n,a,1!=b.level?Math.min(c,b.to)-1:Math.max(s,b.from)).right,m=fm)&&(d=b,p=m)}}return d||(d=i[i.length-1]),d.fromc&&(d={from:d.from,to:c,level:d.level}),d}function oa(n){if(null!=n.cachedTextHeight)return n.cachedTextHeight;if(null==Le){Le=D("pre",null,"CodeMirror-line-like");for(var t=0;t<49;++t)Le.appendChild(document.createTextNode("x")),Le.appendChild(D("br"));Le.appendChild(document.createTextNode("x"))}S(n.measure,Le);var e=Le.offsetHeight/50;return e>3&&(n.cachedTextHeight=e),M(n.measure),e||1}function la(n){if(null!=n.cachedCharWidth)return n.cachedCharWidth;var t=D("span","xxxxxxxxxx"),e=D("pre",[t],"CodeMirror-line-like");S(n.measure,e);var a=t.getBoundingClientRect(),i=(a.right-a.left)/10;return i>2&&(n.cachedCharWidth=i),i||10}function sa(n){for(var t=n.display,e={},a={},i=t.gutters.clientLeft,r=t.gutters.firstChild,o=0;r;r=r.nextSibling,++o){var l=n.display.gutterSpecs[o].className;e[l]=r.offsetLeft+r.clientLeft+i,a[l]=r.clientWidth}return{fixedPos:ca(t),gutterTotalWidth:t.gutters.offsetWidth,gutterLeft:e,gutterWidth:a,wrapperWidth:t.wrapper.clientWidth}}function ca(n){return n.scroller.getBoundingClientRect().left-n.sizer.getBoundingClientRect().left}function da(n){var t=oa(n.display),e=n.options.lineWrapping,a=e&&Math.max(5,n.display.scroller.clientWidth/la(n.display)-3);return function(i){if(Zt(n.doc,i))return 0;var r=0;if(i.widgets)for(var o=0;o0&&(s=Qn(n.doc,c.line).text).length==c.ch){var d=Y(s,s.length,n.options.tabSize)-s.length;c=it(c.line,Math.max(0,Math.round((r-Me(n.display).left)/la(n.display))-d))}return c}function ba(n,t){if(t>=n.display.viewTo)return null;if((t-=n.display.viewFrom)<0)return null;for(var e=n.display.view,a=0;at)&&(i.updateLineNumbers=t),n.curOp.viewChanged=!0,t>=i.viewTo)Ct&&Ht(n.doc,t)i.viewFrom?ga(n):(i.viewFrom+=a,i.viewTo+=a);else if(t<=i.viewFrom&&e>=i.viewTo)ga(n);else if(t<=i.viewFrom){var r=ha(n,e,e+a,1);r?(i.view=i.view.slice(r.index),i.viewFrom=r.lineN,i.viewTo+=a):ga(n)}else if(e>=i.viewTo){var o=ha(n,t,t,-1);o?(i.view=i.view.slice(0,o.index),i.viewTo=o.lineN):ga(n)}else{var l=ha(n,t,t,-1),s=ha(n,e,e+a,1);l&&s?(i.view=i.view.slice(0,l.index).concat(se(n,l.lineN,s.lineN)).concat(i.view.slice(s.index)),i.viewTo+=a):ga(n)}var c=i.externalMeasured;c&&(e=i.lineN&&t=a.viewTo)){var r=a.view[ba(n,t)];if(null!=r.node){var o=r.changes||(r.changes=[]);-1==H(o,e)&&o.push(e)}}}function ga(n){n.display.viewFrom=n.display.viewTo=n.doc.first,n.display.view=[],n.display.viewOffset=0}function ha(n,t,e,a){var i,r=ba(n,t),o=n.display.view;if(!Ct||e==n.doc.first+n.doc.size)return{index:r,lineN:e};for(var l=n.display.viewFrom,s=0;s0){if(r==o.length-1)return null;i=l+o[r].size-t,r++}else i=l-t;t+=i,e+=i}for(;Ht(n.doc,e)!=e;){if(r==(a<0?0:o.length-1))return null;e+=a*o[r-(a<0?1:0)].size,r+=a}return{index:r,lineN:e}}function xa(n){for(var t=n.display.view,e=0,a=0;a=n.display.viewTo||s.to().line0?o:n.defaultCharWidth())+"px"}if(a.other){var l=e.appendChild(D("div"," ","CodeMirror-cursor CodeMirror-secondarycursor"));l.style.display="",l.style.left=a.other.left+"px",l.style.top=a.other.top+"px",l.style.height=.85*(a.other.bottom-a.other.top)+"px"}}function va(n,t){return n.top-t.top||n.left-t.left}function za(n,t,e){var a=n.display,i=n.doc,r=document.createDocumentFragment(),o=Me(n.display),l=o.left,s=Math.max(a.sizerWidth,De(n)-a.sizer.offsetLeft)-o.right,c="ltr"==i.direction;function d(n,t,e,a){t<0&&(t=0),t=Math.round(t),a=Math.round(a),r.appendChild(D("div",null,"CodeMirror-selected","position: absolute; left: "+n+"px;\n top: "+t+"px; width: "+(null==e?s-n:e)+"px;\n height: "+(a-t)+"px"))}function p(t,e,a){var r,o,p=Qn(i,t),u=p.text.length;function b(e,a){return Ge(n,it(t,e),"div",p,a)}function f(t,e,a){var i=ta(n,p,null,t),r="ltr"==e==("after"==a)?"left":"right";return b("after"==a?i.begin:i.end-(/\s/.test(p.text.charAt(i.end-1))?2:1),r)[r]}var m=bn(p,i.direction);return function(n,t,e,a){if(!n)return a(t,e,"ltr",0);for(var i=!1,r=0;rt||t==e&&o.to==t)&&(a(Math.max(o.from,t),Math.min(o.to,e),1==o.level?"rtl":"ltr",r),i=!0)}i||a(t,e,"ltr")}(m,e||0,null==a?u:a,(function(n,t,i,p){var g="ltr"==i,h=b(n,g?"left":"right"),x=b(t-1,g?"right":"left"),w=null==e&&0==n,k=null==a&&t==u,y=0==p,v=!m||p==m.length-1;if(x.top-h.top<=3){var z=(c?k:w)&&v,E=(c?w:k)&&y?l:(g?h:x).left,_=z?s:(g?x:h).right;d(E,h.top,_-E,h.bottom)}else{var C,F,M,S;g?(C=c&&w&&y?l:h.left,F=c?s:f(n,i,"before"),M=c?l:f(t,i,"after"),S=c&&k&&v?s:x.right):(C=c?f(n,i,"before"):l,F=!c&&w&&y?s:h.right,M=!c&&k&&v?l:x.left,S=c?f(t,i,"after"):s),d(C,h.top,F-C,h.bottom),h.bottom0?t.blinker=setInterval((function(){n.hasFocus()||Ma(n),t.cursorDiv.style.visibility=(e=!e)?"":"hidden"}),n.options.cursorBlinkRate):n.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function _a(n){n.hasFocus()||(n.display.input.focus(),n.state.focused||Fa(n))}function Ca(n){n.state.delayingBlurEvent=!0,setTimeout((function(){n.state.delayingBlurEvent&&(n.state.delayingBlurEvent=!1,n.state.focused&&Ma(n))}),100)}function Fa(n,t){n.state.delayingBlurEvent&&!n.state.draggingText&&(n.state.delayingBlurEvent=!1),"nocursor"!=n.options.readOnly&&(n.state.focused||(xn(n,"focus",n,t),n.state.focused=!0,N(n.display.wrapper,"CodeMirror-focused"),n.curOp||n.display.selForContextMenu==n.doc.sel||(n.display.input.reset(),s&&setTimeout((function(){return n.display.input.reset(!0)}),20)),n.display.input.receivedFocus()),Ea(n))}function Ma(n,t){n.state.delayingBlurEvent||(n.state.focused&&(xn(n,"blur",n,t),n.state.focused=!1,F(n.display.wrapper,"CodeMirror-focused")),clearInterval(n.display.blinker),setTimeout((function(){n.state.focused||(n.display.shift=!1)}),150))}function Sa(n){for(var t=n.display,e=t.lineDiv.offsetTop,a=Math.max(0,t.scroller.getBoundingClientRect().top),i=t.lineDiv.getBoundingClientRect().top,r=0,s=0;s.005||m<-.005)&&(in.display.sizerWidth){var h=Math.ceil(u/la(n.display));h>n.display.maxLineLength&&(n.display.maxLineLength=h,n.display.maxLine=c.line,n.display.maxLineChanged=!0)}}}Math.abs(r)>2&&(t.scroller.scrollTop+=r)}function Da(n){if(n.widgets)for(var t=0;t=o&&(r=tt(t,Xt(Qn(t,s))-n.wrapper.clientHeight),o=s)}return{from:r,to:Math.max(o,r+1)}}function Oa(n,t){var e=n.display,a=oa(n.display);t.top<0&&(t.top=0);var i=n.curOp&&null!=n.curOp.scrollTop?n.curOp.scrollTop:e.scroller.scrollTop,r=je(n),o={};t.bottom-t.top>r&&(t.bottom=t.top+r);var l=n.doc.height+Fe(e),s=t.topl-a;if(t.topi+r){var d=Math.min(t.top,(c?l:t.bottom)-r);d!=i&&(o.scrollTop=d)}var p=n.options.fixedGutter?0:e.gutters.offsetWidth,u=n.curOp&&null!=n.curOp.scrollLeft?n.curOp.scrollLeft:e.scroller.scrollLeft-p,b=De(n)-e.gutters.offsetWidth,f=t.right-t.left>b;return f&&(t.right=t.left+b),t.left<10?o.scrollLeft=0:t.leftb+u-3&&(o.scrollLeft=t.right+(f?0:10)-b),o}function Ia(n,t){null!=t&&(Ta(n),n.curOp.scrollTop=(null==n.curOp.scrollTop?n.doc.scrollTop:n.curOp.scrollTop)+t)}function Na(n){Ta(n);var t=n.getCursor();n.curOp.scrollToPos={from:t,to:t,margin:n.options.cursorScrollMargin}}function Aa(n,t,e){null==t&&null==e||Ta(n),null!=t&&(n.curOp.scrollLeft=t),null!=e&&(n.curOp.scrollTop=e)}function Ta(n){var t=n.curOp.scrollToPos;t&&(n.curOp.scrollToPos=null,La(n,Ke(n,t.from),Ke(n,t.to),t.margin))}function La(n,t,e,a){var i=Oa(n,{left:Math.min(t.left,e.left),top:Math.min(t.top,e.top)-a,right:Math.max(t.right,e.right),bottom:Math.max(t.bottom,e.bottom)+a});Aa(n,i.scrollLeft,i.scrollTop)}function Pa(n,t){Math.abs(n.doc.scrollTop-t)<2||(e||ui(n,{top:t}),Ba(n,t,!0),e&&ui(n),oi(n,100))}function Ba(n,t,e){t=Math.max(0,Math.min(n.display.scroller.scrollHeight-n.display.scroller.clientHeight,t)),(n.display.scroller.scrollTop!=t||e)&&(n.doc.scrollTop=t,n.display.scrollbars.setScrollTop(t),n.display.scroller.scrollTop!=t&&(n.display.scroller.scrollTop=t))}function Ra(n,t,e,a){t=Math.max(0,Math.min(t,n.display.scroller.scrollWidth-n.display.scroller.clientWidth)),(e?t==n.doc.scrollLeft:Math.abs(n.doc.scrollLeft-t)<2)&&!a||(n.doc.scrollLeft=t,mi(n),n.display.scroller.scrollLeft!=t&&(n.display.scroller.scrollLeft=t),n.display.scrollbars.setScrollLeft(t))}function Ya(n){var t=n.display,e=t.gutters.offsetWidth,a=Math.round(n.doc.height+Fe(n.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:n.options.fixedGutter?e:0,docHeight:a,scrollHeight:a+Se(n)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:e}}var Ua=function(n,t,e){this.cm=e;var a=this.vert=D("div",[D("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=D("div",[D("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=i.tabIndex=-1,n(a),n(i),mn(a,"scroll",(function(){a.clientHeight&&t(a.scrollTop,"vertical")})),mn(i,"scroll",(function(){i.clientWidth&&t(i.scrollLeft,"horizontal")})),this.checkedZeroWidth=!1,o&&l<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};Ua.prototype.update=function(n){var t=n.scrollWidth>n.clientWidth+1,e=n.scrollHeight>n.clientHeight+1,a=n.nativeBarWidth;if(e){this.vert.style.display="block",this.vert.style.bottom=t?a+"px":"0";var i=n.viewHeight-(t?a:0);this.vert.firstChild.style.height=Math.max(0,n.scrollHeight-n.clientHeight+i)+"px"}else this.vert.scrollTop=0,this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=e?a+"px":"0",this.horiz.style.left=n.barLeft+"px";var r=n.viewWidth-n.barLeft-(e?a:0);this.horiz.firstChild.style.width=Math.max(0,n.scrollWidth-n.clientWidth+r)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&n.clientHeight>0&&(0==a&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:e?a:0,bottom:t?a:0}},Ua.prototype.setScrollLeft=function(n){this.horiz.scrollLeft!=n&&(this.horiz.scrollLeft=n),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Ua.prototype.setScrollTop=function(n){this.vert.scrollTop!=n&&(this.vert.scrollTop=n),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Ua.prototype.zeroWidthHack=function(){var n=w&&!f?"12px":"18px";this.horiz.style.height=this.vert.style.width=n,this.horiz.style.visibility=this.vert.style.visibility="hidden",this.disableHoriz=new U,this.disableVert=new U},Ua.prototype.enableZeroWidthBar=function(n,t,e){n.style.visibility="",t.set(1e3,(function a(){var i=n.getBoundingClientRect();("vert"==e?document.elementFromPoint(i.right-1,(i.top+i.bottom)/2):document.elementFromPoint((i.right+i.left)/2,i.bottom-1))!=n?n.style.visibility="hidden":t.set(1e3,a)}))},Ua.prototype.clear=function(){var n=this.horiz.parentNode;n.removeChild(this.horiz),n.removeChild(this.vert)};var Ha=function(){};function Wa(n,t){t||(t=Ya(n));var e=n.display.barWidth,a=n.display.barHeight;Za(n,t);for(var i=0;i<4&&e!=n.display.barWidth||a!=n.display.barHeight;i++)e!=n.display.barWidth&&n.options.lineWrapping&&Sa(n),Za(n,Ya(n)),e=n.display.barWidth,a=n.display.barHeight}function Za(n,t){var e=n.display,a=e.scrollbars.update(t);e.sizer.style.paddingRight=(e.barWidth=a.right)+"px",e.sizer.style.paddingBottom=(e.barHeight=a.bottom)+"px",e.heightForcer.style.borderBottom=a.bottom+"px solid transparent",a.right&&a.bottom?(e.scrollbarFiller.style.display="block",e.scrollbarFiller.style.height=a.bottom+"px",e.scrollbarFiller.style.width=a.right+"px"):e.scrollbarFiller.style.display="",a.bottom&&n.options.coverGutterNextToScrollbar&&n.options.fixedGutter?(e.gutterFiller.style.display="block",e.gutterFiller.style.height=a.bottom+"px",e.gutterFiller.style.width=t.gutterWidth+"px"):e.gutterFiller.style.display=""}Ha.prototype.update=function(){return{bottom:0,right:0}},Ha.prototype.setScrollLeft=function(){},Ha.prototype.setScrollTop=function(){},Ha.prototype.clear=function(){};var Va={native:Ua,null:Ha};function Xa(n){n.display.scrollbars&&(n.display.scrollbars.clear(),n.display.scrollbars.addClass&&F(n.display.wrapper,n.display.scrollbars.addClass)),n.display.scrollbars=new Va[n.options.scrollbarStyle]((function(t){n.display.wrapper.insertBefore(t,n.display.scrollbarFiller),mn(t,"mousedown",(function(){n.state.focused&&setTimeout((function(){return n.display.input.focus()}),0)})),t.setAttribute("cm-not-content","true")}),(function(t,e){"horizontal"==e?Ra(n,t):Pa(n,t)}),n),n.display.scrollbars.addClass&&N(n.display.wrapper,n.display.scrollbars.addClass)}var qa=0;function Ga(n){var t;n.curOp={cm:n,viewChanged:!1,startHeight:n.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++qa,markArrays:null},t=n.curOp,ce?ce.ops.push(t):t.ownsGroup=ce={ops:[t],delayedCallbacks:[]}}function Qa(n){var t=n.curOp;t&&function(n,t){var e=n.ownsGroup;if(e)try{!function(n){var t=n.delayedCallbacks,e=0;do{for(;e=e.viewTo)||e.maxLineChanged&&t.options.lineWrapping,n.update=n.mustUpdate&&new si(t,n.mustUpdate&&{top:n.scrollTop,ensure:n.scrollToPos},n.forceUpdate)}function Ja(n){n.updatedDisplay=n.mustUpdate&&di(n.cm,n.update)}function $a(n){var t=n.cm,e=t.display;n.updatedDisplay&&Sa(t),n.barMeasure=Ya(t),e.maxLineChanged&&!t.options.lineWrapping&&(n.adjustWidthTo=Ie(t,e.maxLine,e.maxLine.text.length).left+3,t.display.sizerWidth=n.adjustWidthTo,n.barMeasure.scrollWidth=Math.max(e.scroller.clientWidth,e.sizer.offsetLeft+n.adjustWidthTo+Se(t)+t.display.barWidth),n.maxScrollLeft=Math.max(0,e.sizer.offsetLeft+n.adjustWidthTo-De(t))),(n.updatedDisplay||n.selectionChanged)&&(n.preparedSelection=e.input.prepareSelection())}function ni(n){var t=n.cm;null!=n.adjustWidthTo&&(t.display.sizer.style.minWidth=n.adjustWidthTo+"px",n.maxScrollLeft(r.defaultView.innerHeight||r.documentElement.clientHeight)&&(i=!1),null!=i&&!m){var o=D("div","​",null,"position: absolute;\n top: "+(t.top-e.viewOffset-Ce(n.display))+"px;\n height: "+(t.bottom-t.top+Se(n)+e.barHeight)+"px;\n left: "+t.left+"px; width: "+Math.max(2,t.right-t.left)+"px;");n.display.lineSpace.appendChild(o),o.scrollIntoView(i),n.display.lineSpace.removeChild(o)}}}(t,function(n,t,e,a){var i;null==a&&(a=0),n.options.lineWrapping||t!=e||(e="before"==t.sticky?it(t.line,t.ch+1,"before"):t,t=t.ch?it(t.line,"before"==t.sticky?t.ch-1:t.ch,"after"):t);for(var r=0;r<5;r++){var o=!1,l=Qe(n,t),s=e&&e!=t?Qe(n,e):l,c=Oa(n,i={left:Math.min(l.left,s.left),top:Math.min(l.top,s.top)-a,right:Math.max(l.left,s.left),bottom:Math.max(l.bottom,s.bottom)+a}),d=n.doc.scrollTop,p=n.doc.scrollLeft;if(null!=c.scrollTop&&(Pa(n,c.scrollTop),Math.abs(n.doc.scrollTop-d)>1&&(o=!0)),null!=c.scrollLeft&&(Ra(n,c.scrollLeft),Math.abs(n.doc.scrollLeft-p)>1&&(o=!0)),!o)break}return i}(t,pt(a,n.scrollToPos.from),pt(a,n.scrollToPos.to),n.scrollToPos.margin));var i=n.maybeHiddenMarkers,r=n.maybeUnhiddenMarkers;if(i)for(var o=0;o=n.display.viewTo)){var e=+new Date+n.options.workTime,a=ht(n,t.highlightFrontier),i=[];t.iter(a.line,Math.min(t.first+t.size,n.display.viewTo+500),(function(r){if(a.line>=n.display.viewFrom){var o=r.styles,l=r.text.length>n.options.maxHighlightLength?Vn(t.mode,a.state):null,s=mt(n,r,a,!0);l&&(a.state=l),r.styles=s.styles;var c=r.styleClasses,d=s.classes;d?r.styleClasses=d:c&&(r.styleClasses=null);for(var p=!o||o.length!=r.styles.length||c!=d&&(!c||!d||c.bgClass!=d.bgClass||c.textClass!=d.textClass),u=0;!p&&ue)return oi(n,n.options.workDelay),!0})),t.highlightFrontier=a.line,t.modeFrontier=Math.max(t.modeFrontier,a.line),i.length&&ei(n,(function(){for(var t=0;t=e.viewFrom&&t.visible.to<=e.viewTo&&(null==e.updateLineNumbers||e.updateLineNumbers>=e.viewTo)&&e.renderedView==e.view&&0==xa(n))return!1;gi(n)&&(ga(n),t.dims=sa(n));var i=a.first+a.size,r=Math.max(t.visible.from-n.options.viewportMargin,a.first),o=Math.min(i,t.visible.to+n.options.viewportMargin);e.viewFromo&&e.viewTo-o<20&&(o=Math.min(i,e.viewTo)),Ct&&(r=Ht(n.doc,r),o=Wt(n.doc,o));var l=r!=e.viewFrom||o!=e.viewTo||e.lastWrapHeight!=t.wrapperHeight||e.lastWrapWidth!=t.wrapperWidth;!function(n,t,e){var a=n.display;0==a.view.length||t>=a.viewTo||e<=a.viewFrom?(a.view=se(n,t,e),a.viewFrom=t):(a.viewFrom>t?a.view=se(n,t,a.viewFrom).concat(a.view):a.viewFrome&&(a.view=a.view.slice(0,ba(n,e)))),a.viewTo=e}(n,r,o),e.viewOffset=Xt(Qn(n.doc,e.viewFrom)),n.display.mover.style.top=e.viewOffset+"px";var c=xa(n);if(!l&&0==c&&!t.force&&e.renderedView==e.view&&(null==e.updateLineNumbers||e.updateLineNumbers>=e.viewTo))return!1;var d=ci(n);return c>4&&(e.lineDiv.style.display="none"),function(n,t,e){var a=n.display,i=n.options.lineNumbers,r=a.lineDiv,o=r.firstChild;function l(t){var e=t.nextSibling;return s&&w&&n.display.currentWheelTarget==t?t.style.display="none":t.parentNode.removeChild(t),e}for(var c=a.view,d=a.viewFrom,p=0;p-1&&(b=!1),be(n,u,d,e)),b&&(M(u.lineNumber),u.lineNumber.appendChild(document.createTextNode(at(n.options,d)))),o=u.node.nextSibling}else{var f=ke(n,u,d,e);r.insertBefore(f,o)}d+=u.size}for(;o;)o=l(o)}(n,e.updateLineNumbers,t.dims),c>4&&(e.lineDiv.style.display=""),e.renderedView=e.view,function(n){if(n&&n.activeElt&&n.activeElt!=I(n.activeElt.ownerDocument)&&(n.activeElt.focus(),!/^(INPUT|TEXTAREA)$/.test(n.activeElt.nodeName)&&n.anchorNode&&O(document.body,n.anchorNode)&&O(document.body,n.focusNode))){var t=n.activeElt.ownerDocument,e=t.defaultView.getSelection(),a=t.createRange();a.setEnd(n.anchorNode,n.anchorOffset),a.collapse(!1),e.removeAllRanges(),e.addRange(a),e.extend(n.focusNode,n.focusOffset)}}(d),M(e.cursorDiv),M(e.selectionDiv),e.gutters.style.height=e.sizer.style.minHeight=0,l&&(e.lastWrapHeight=t.wrapperHeight,e.lastWrapWidth=t.wrapperWidth,oi(n,400)),e.updateLineNumbers=null,!0}function pi(n,t){for(var e=t.viewport,a=!0;;a=!1){if(a&&n.options.lineWrapping&&t.oldDisplayWidth!=De(n))a&&(t.visible=ja(n.display,n.doc,e));else if(e&&null!=e.top&&(e={top:Math.min(n.doc.height+Fe(n.display)-je(n),e.top)}),t.visible=ja(n.display,n.doc,e),t.visible.from>=n.display.viewFrom&&t.visible.to<=n.display.viewTo)break;if(!di(n,t))break;Sa(n);var i=Ya(n);wa(n),Wa(n,i),fi(n,i),t.force=!1}t.signal(n,"update",n),n.display.viewFrom==n.display.reportedViewFrom&&n.display.viewTo==n.display.reportedViewTo||(t.signal(n,"viewportChange",n,n.display.viewFrom,n.display.viewTo),n.display.reportedViewFrom=n.display.viewFrom,n.display.reportedViewTo=n.display.viewTo)}function ui(n,t){var e=new si(n,t);if(di(n,e)){Sa(n),pi(n,e);var a=Ya(n);wa(n),Wa(n,a),fi(n,a),e.finish()}}function bi(n){var t=n.gutters.offsetWidth;n.sizer.style.marginLeft=t+"px",pe(n,"gutterChanged",n)}function fi(n,t){n.display.sizer.style.minHeight=t.docHeight+"px",n.display.heightForcer.style.top=t.docHeight+"px",n.display.gutters.style.height=t.docHeight+n.display.barHeight+Se(n)+"px"}function mi(n){var t=n.display,e=t.view;if(t.alignWidgets||t.gutters.firstChild&&n.options.fixedGutter){for(var a=ca(t)-t.scroller.scrollLeft+n.doc.scrollLeft,i=t.gutters.offsetWidth,r=a+"px",o=0;o=105&&(r.wrapper.style.clipPath="inset(0px)"),r.wrapper.setAttribute("translate","no"),o&&l<8&&(r.gutters.style.zIndex=-1,r.scroller.style.paddingRight=0),s||e&&x||(r.scroller.draggable=!0),n&&(n.appendChild?n.appendChild(r.wrapper):n(r.wrapper)),r.viewFrom=r.viewTo=t.first,r.reportedViewFrom=r.reportedViewTo=t.first,r.view=[],r.renderedView=null,r.externalMeasured=null,r.viewOffset=0,r.lastWrapHeight=r.lastWrapWidth=0,r.updateLineNumbers=null,r.nativeBarWidth=r.barHeight=r.barWidth=0,r.scrollbarsClipped=!1,r.lineNumWidth=r.lineNumInnerWidth=r.lineNumChars=null,r.alignWidgets=!1,r.cachedCharWidth=r.cachedTextHeight=r.cachedPaddingH=null,r.maxLine=null,r.maxLineLength=0,r.maxLineChanged=!1,r.wheelDX=r.wheelDY=r.wheelStartX=r.wheelStartY=null,r.shift=!1,r.selForContextMenu=null,r.activeTouch=null,r.gutterSpecs=hi(i.gutters,i.lineNumbers),xi(r),a.init(r)}si.prototype.signal=function(n,t){yn(n,t)&&this.events.push(arguments)},si.prototype.finish=function(){for(var n=0;nc.clientWidth,f=c.scrollHeight>c.clientHeight;if(i&&b||r&&f){if(r&&w&&s)n:for(var m=t.target,g=l.view;m!=c;m=m.parentNode)for(var h=0;h=0&&rt(n,a.to())<=0)return e}return-1};var Fi=function(n,t){this.anchor=n,this.head=t};function Mi(n,t,e){var a=n&&n.options.selectionsMayTouch,i=t[e];t.sort((function(n,t){return rt(n.from(),t.from())})),e=H(t,i);for(var r=1;r0:s>=0){var c=ct(l.from(),o.from()),d=st(l.to(),o.to()),p=l.empty()?o.from()==o.head:l.from()==l.head;r<=e&&--e,t.splice(--r,2,new Fi(p?d:c,p?c:d))}}return new Ci(t,e)}function Si(n,t){return new Ci([new Fi(n,t||n)],0)}function Di(n){return n.text?it(n.from.line+n.text.length-1,K(n.text).length+(1==n.text.length?n.from.ch:0)):n.to}function ji(n,t){if(rt(n,t.from)<0)return n;if(rt(n,t.to)<=0)return Di(t);var e=n.line+t.text.length-(t.to.line-t.from.line)-1,a=n.ch;return n.line==t.to.line&&(a+=Di(t).ch-t.to.ch),it(e,a)}function Oi(n,t){for(var e=[],a=0;a1&&n.remove(l.line+1,f-1),n.insert(l.line+1,h)}pe(n,"change",n,t)}function Pi(n,t,e){!function n(a,i,r){if(a.linked)for(var o=0;ol-(n.cm?n.cm.options.historyEventDelay:500)||"*"==t.origin.charAt(0)))&&(r=function(n,t){return t?(Hi(n.done),K(n.done)):n.done.length&&!K(n.done).ranges?K(n.done):n.done.length>1&&!n.done[n.done.length-2].ranges?(n.done.pop(),K(n.done)):void 0}(i,i.lastOp==a)))o=K(r.changes),0==rt(t.from,t.to)&&0==rt(t.from,o.to)?o.to=Di(t):r.changes.push(Ui(n,t));else{var s=K(i.done);for(s&&s.ranges||Vi(n.sel,i.done),r={changes:[Ui(n,t)],generation:i.generation},i.done.push(r);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(e),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=l,i.lastOp=i.lastSelOp=a,i.lastOrigin=i.lastSelOrigin=t.origin,o||xn(n,"historyAdded")}function Zi(n,t,e,a){var i=n.history,r=a&&a.origin;e==i.lastSelOp||r&&i.lastSelOrigin==r&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==r||function(n,t,e,a){var i=t.charAt(0);return"*"==i||"+"==i&&e.ranges.length==a.ranges.length&&e.somethingSelected()==a.somethingSelected()&&new Date-n.history.lastSelTime<=(n.cm?n.cm.options.historyEventDelay:500)}(n,r,K(i.done),t))?i.done[i.done.length-1]=t:Vi(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=r,i.lastSelOp=e,a&&!1!==a.clearRedo&&Hi(i.undone)}function Vi(n,t){var e=K(t);e&&e.ranges&&e.equals(n)||t.push(n)}function Xi(n,t,e,a){var i=t["spans_"+n.id],r=0;n.iter(Math.max(n.first,e),Math.min(n.first+n.size,a),(function(e){e.markedSpans&&((i||(i=t["spans_"+n.id]={}))[r]=e.markedSpans),++r}))}function qi(n){if(!n)return null;for(var t,e=0;e-1&&(K(l)[p]=c[p],delete c[p])}}}return a}function Ki(n,t,e,a){if(a){var i=n.anchor;if(e){var r=rt(t,i)<0;r!=rt(e,i)<0?(i=t,t=e):r!=rt(t,e)<0&&(t=e)}return new Fi(i,t)}return new Fi(e||t,t)}function Ji(n,t,e,a,i){null==i&&(i=n.cm&&(n.cm.display.shift||n.extend)),ar(n,new Ci([Ki(n.sel.primary(),t,e,i)],0),a)}function $i(n,t,e){for(var a=[],i=n.cm&&(n.cm.display.shift||n.extend),r=0;r=t.ch:l.to>t.ch))){if(i&&(xn(s,"beforeCursorEnter"),s.explicitlyCleared)){if(r.markedSpans){--o;continue}break}if(!s.atomic)continue;if(e){var p=s.find(a<0?1:-1),u=void 0;if((a<0?d:c)&&(p=dr(n,p,-a,p&&p.line==t.line?r:null)),p&&p.line==t.line&&(u=rt(p,e))&&(a<0?u<0:u>0))return sr(n,p,t,a,i)}var b=s.find(a<0?-1:1);return(a<0?c:d)&&(b=dr(n,b,a,b.line==t.line?r:null)),b?sr(n,b,t,a,i):null}}return t}function cr(n,t,e,a,i){var r=a||1,o=sr(n,t,e,r,i)||!i&&sr(n,t,e,r,!0)||sr(n,t,e,-r,i)||!i&&sr(n,t,e,-r,!0);return o||(n.cantEdit=!0,it(n.first,0))}function dr(n,t,e,a){return e<0&&0==t.ch?t.line>n.first?pt(n,it(t.line-1)):null:e>0&&t.ch==(a||Qn(n,t.line)).text.length?t.line0)){var d=[s,1],p=rt(c.from,l.from),u=rt(c.to,l.to);(p<0||!o.inclusiveLeft&&!p)&&d.push({from:c.from,to:l.from}),(u>0||!o.inclusiveRight&&!u)&&d.push({from:l.to,to:c.to}),i.splice.apply(i,d),s+=d.length-3}}return i}(n,t.from,t.to);if(a)for(var i=a.length-1;i>=0;--i)fr(n,{from:a[i].from,to:a[i].to,text:i?[""]:t.text,origin:t.origin});else fr(n,t)}}function fr(n,t){if(1!=t.text.length||""!=t.text[0]||0!=rt(t.from,t.to)){var e=Oi(n,t);Wi(n,t,e,n.cm?n.cm.curOp.id:NaN),hr(n,t,e,Dt(n,t));var a=[];Pi(n,(function(n,e){e||-1!=H(a,n.history)||(yr(n.history,t),a.push(n.history)),hr(n,t,null,Dt(n,t))}))}}function mr(n,t,e){var a=n.cm&&n.cm.state.suppressEdits;if(!a||e){for(var i,r=n.history,o=n.sel,l="undo"==t?r.done:r.undone,s="undo"==t?r.undone:r.done,c=0;c=0;--b){var f=u(b);if(f)return f.v}}}}function gr(n,t){if(0!=t&&(n.first+=t,n.sel=new Ci(J(n.sel.ranges,(function(n){return new Fi(it(n.anchor.line+t,n.anchor.ch),it(n.head.line+t,n.head.ch))})),n.sel.primIndex),n.cm)){fa(n.cm,n.first,n.first-t,t);for(var e=n.cm.display,a=e.viewFrom;an.lastLine())){if(t.from.liner&&(t={from:t.from,to:it(r,Qn(n,r).text.length),text:[t.text[0]],origin:t.origin}),t.removed=Kn(n,t.from,t.to),e||(e=Oi(n,t)),n.cm?function(n,t,e){var a=n.doc,i=n.display,r=t.from,o=t.to,l=!1,s=r.line;n.options.lineWrapping||(s=nt(Ut(Qn(a,r.line))),a.iter(s,o.line+1,(function(n){if(n==i.maxLine)return l=!0,!0}))),a.sel.contains(t.from,t.to)>-1&&kn(n),Li(a,t,e,da(n)),n.options.lineWrapping||(a.iter(s,r.line+t.text.length,(function(n){var t=qt(n);t>i.maxLineLength&&(i.maxLine=n,i.maxLineLength=t,i.maxLineChanged=!0,l=!1)})),l&&(n.curOp.updateMaxLine=!0)),function(n,t){if(n.modeFrontier=Math.min(n.modeFrontier,t),!(n.highlightFrontiere;a--){var i=Qn(n,a).stateAfter;if(i&&(!(i instanceof bt)||a+i.lookAhead1||!(this.children[0]instanceof zr))){var l=[];this.collapse(l),this.children=[new zr(l)],this.children[0].parent=this}},collapse:function(n){for(var t=0;t50){for(var o=i.lines.length%25+25,l=o;l10);n.parent.maybeSpill()}},iterN:function(n,t,e){for(var a=0;a0||0==o&&!1!==r.clearWhenEmpty)return r;if(r.replacedWith&&(r.collapsed=!0,r.widgetNode=j("span",[r.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||r.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(r.widgetNode.insertLeft=!0)),r.collapsed){if(Yt(n,t.line,t,e,r)||t.line!=e.line&&Yt(n,e.line,t,e,r))throw new Error("Inserting collapsed marker partially overlapping an existing one");Ct=!0}r.addToHistory&&Wi(n,{from:t,to:e,origin:"markText"},n.sel,NaN);var l,s=t.line,c=n.cm;if(n.iter(s,e.line+1,(function(a){c&&r.collapsed&&!c.options.lineWrapping&&Ut(a)==c.display.maxLine&&(l=!0),r.collapsed&&s!=t.line&&$n(a,0),function(n,t,e){var a=e&&window.WeakSet&&(e.markedSpans||(e.markedSpans=new WeakSet));a&&n.markedSpans&&a.has(n.markedSpans)?n.markedSpans.push(t):(n.markedSpans=n.markedSpans?n.markedSpans.concat([t]):[t],a&&a.add(n.markedSpans)),t.marker.attachLine(n)}(a,new Ft(r,s==t.line?t.ch:null,s==e.line?e.ch:null),n.cm&&n.cm.curOp),++s})),r.collapsed&&n.iter(t.line,e.line+1,(function(t){Zt(n,t)&&$n(t,0)})),r.clearOnEnter&&mn(r,"beforeCursorEnter",(function(){return r.clear()})),r.readOnly&&(_t=!0,(n.history.done.length||n.history.undone.length)&&n.clearHistory()),r.collapsed&&(r.id=++Fr,r.atomic=!0),c){if(l&&(c.curOp.updateMaxLine=!0),r.collapsed)fa(c,t.line,e.line+1);else if(r.className||r.startStyle||r.endStyle||r.css||r.attributes||r.title)for(var d=t.line;d<=e.line;d++)ma(c,d,"text");r.atomic&&or(c.doc),pe(c,"markerAdded",c,r)}return r}Mr.prototype.clear=function(){if(!this.explicitlyCleared){var n=this.doc.cm,t=n&&!n.curOp;if(t&&Ga(n),yn(this,"clear")){var e=this.find();e&&pe(this,"clear",e.from,e.to)}for(var a=null,i=null,r=0;rn.display.maxLineLength&&(n.display.maxLine=c,n.display.maxLineLength=d,n.display.maxLineChanged=!0)}null!=a&&n&&this.collapsed&&fa(n,a,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,n&&or(n.doc)),n&&pe(n,"markerCleared",n,this,a,i),t&&Qa(n),this.parent&&this.parent.clear()}},Mr.prototype.find=function(n,t){var e,a;null==n&&"bookmark"==this.type&&(n=1);for(var i=0;i=0;s--)br(this,a[s]);l?er(this,l):this.cm&&Na(this.cm)})),undo:ri((function(){mr(this,"undo")})),redo:ri((function(){mr(this,"redo")})),undoSelection:ri((function(){mr(this,"undo",!0)})),redoSelection:ri((function(){mr(this,"redo",!0)})),setExtending:function(n){this.extend=n},getExtending:function(){return this.extend},historySize:function(){for(var n=this.history,t=0,e=0,a=0;a=n.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(n,t,e){n=pt(this,n),t=pt(this,t);var a=[],i=n.line;return this.iter(n.line,t.line+1,(function(r){var o=r.markedSpans;if(o)for(var l=0;l=s.to||null==s.from&&i!=n.line||null!=s.from&&i==t.line&&s.from>=t.ch||e&&!e(s.marker)||a.push(s.marker.parent||s.marker)}++i})),a},getAllMarks:function(){var n=[];return this.iter((function(t){var e=t.markedSpans;if(e)for(var a=0;an)return t=n,!0;n-=r,++e})),pt(this,it(e,t))},indexFromPos:function(n){var t=(n=pt(this,n)).ch;if(n.linet&&(t=n.from),null!=n.to&&n.to-1)return t.state.draggingText(n),void setTimeout((function(){return t.display.input.focus()}),20);try{var p=n.dataTransfer.getData("Text");if(p){var u;if(t.state.draggingText&&!t.state.draggingText.copy&&(u=t.listSelections()),ir(t.doc,Si(e,e)),u)for(var b=0;b=0;t--)xr(n.doc,"",a[t].from,a[t].to,"+delete");Na(n)}))}function to(n,t,e){var a=sn(n.text,t+e,e);return a<0||a>n.text.length?null:a}function eo(n,t,e){var a=to(n,t.ch,e);return null==a?null:new it(t.line,a,e<0?"after":"before")}function ao(n,t,e,a,i){if(n){"rtl"==t.doc.direction&&(i=-i);var r=bn(e,t.doc.direction);if(r){var o,l=i<0?K(r):r[0],s=i<0==(1==l.level)?"after":"before";if(l.level>0||"rtl"==t.doc.direction){var c=Ae(t,e);o=i<0?e.text.length-1:0;var d=Te(t,c,o).top;o=cn((function(n){return Te(t,c,n).top==d}),i<0==(1==l.level)?l.from:l.to-1,o),"before"==s&&(o=to(e,o,1))}else o=i<0?l.to:l.from;return new it(a,o,s)}}return new it(a,i<0?e.text.length:0,i<0?"before":"after")}Vr.basic={Left:"goCharLeft",Right:"goCharRight",Up:"goLineUp",Down:"goLineDown",End:"goLineEnd",Home:"goLineStartSmart",PageUp:"goPageUp",PageDown:"goPageDown",Delete:"delCharAfter",Backspace:"delCharBefore","Shift-Backspace":"delCharBefore",Tab:"defaultTab","Shift-Tab":"indentAuto",Enter:"newlineAndIndent",Insert:"toggleOverwrite",Esc:"singleSelection"},Vr.pcDefault={"Ctrl-A":"selectAll","Ctrl-D":"deleteLine","Ctrl-Z":"undo","Shift-Ctrl-Z":"redo","Ctrl-Y":"redo","Ctrl-Home":"goDocStart","Ctrl-End":"goDocEnd","Ctrl-Up":"goLineUp","Ctrl-Down":"goLineDown","Ctrl-Left":"goGroupLeft","Ctrl-Right":"goGroupRight","Alt-Left":"goLineStart","Alt-Right":"goLineEnd","Ctrl-Backspace":"delGroupBefore","Ctrl-Delete":"delGroupAfter","Ctrl-S":"save","Ctrl-F":"find","Ctrl-G":"findNext","Shift-Ctrl-G":"findPrev","Shift-Ctrl-F":"replace","Shift-Ctrl-R":"replaceAll","Ctrl-[":"indentLess","Ctrl-]":"indentMore","Ctrl-U":"undoSelection","Shift-Ctrl-U":"redoSelection","Alt-U":"redoSelection",fallthrough:"basic"},Vr.emacsy={"Ctrl-F":"goCharRight","Ctrl-B":"goCharLeft","Ctrl-P":"goLineUp","Ctrl-N":"goLineDown","Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd","Ctrl-V":"goPageDown","Shift-Ctrl-V":"goPageUp","Ctrl-D":"delCharAfter","Ctrl-H":"delCharBefore","Alt-Backspace":"delWordBefore","Ctrl-K":"killLine","Ctrl-T":"transposeChars","Ctrl-O":"openLine"},Vr.macDefault={"Cmd-A":"selectAll","Cmd-D":"deleteLine","Cmd-Z":"undo","Shift-Cmd-Z":"redo","Cmd-Y":"redo","Cmd-Home":"goDocStart","Cmd-Up":"goDocStart","Cmd-End":"goDocEnd","Cmd-Down":"goDocEnd","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight","Cmd-Left":"goLineLeft","Cmd-Right":"goLineRight","Alt-Backspace":"delGroupBefore","Ctrl-Alt-Backspace":"delGroupAfter","Alt-Delete":"delGroupAfter","Cmd-S":"save","Cmd-F":"find","Cmd-G":"findNext","Shift-Cmd-G":"findPrev","Cmd-Alt-F":"replace","Shift-Cmd-Alt-F":"replaceAll","Cmd-[":"indentLess","Cmd-]":"indentMore","Cmd-Backspace":"delWrappedLineLeft","Cmd-Delete":"delWrappedLineRight","Cmd-U":"undoSelection","Shift-Cmd-U":"redoSelection","Ctrl-Up":"goDocStart","Ctrl-Down":"goDocEnd",fallthrough:["basic","emacsy"]},Vr.default=w?Vr.macDefault:Vr.pcDefault;var io={selectAll:pr,singleSelection:function(n){return n.setSelection(n.getCursor("anchor"),n.getCursor("head"),Z)},killLine:function(n){return no(n,(function(t){if(t.empty()){var e=Qn(n.doc,t.head.line).text.length;return t.head.ch==e&&t.head.line0)i=new it(i.line,i.ch+1),n.replaceRange(r.charAt(i.ch-1)+r.charAt(i.ch-2),it(i.line,i.ch-2),i,"+transpose");else if(i.line>n.doc.first){var o=Qn(n.doc,i.line-1).text;o&&(i=new it(i.line,1),n.replaceRange(r.charAt(0)+n.doc.lineSeparator()+o.charAt(o.length-1),it(i.line-1,o.length-1),i,"+transpose"))}e.push(new Fi(i,i))}n.setSelections(e)}))},newlineAndIndent:function(n){return ei(n,(function(){for(var t=n.listSelections(),e=t.length-1;e>=0;e--)n.replaceRange(n.doc.lineSeparator(),t[e].anchor,t[e].head,"+input");t=n.listSelections();for(var a=0;a-1&&(rt((i=c.ranges[i]).from(),t)<0||t.xRel>0)&&(rt(i.to(),t)>0||t.xRel<0)?function(n,t,e,a){var i=n.display,r=!1,c=ai(n,(function(t){s&&(i.scroller.draggable=!1),n.state.draggingText=!1,n.state.delayingBlurEvent&&(n.hasFocus()?n.state.delayingBlurEvent=!1:Ca(n)),hn(i.wrapper.ownerDocument,"mouseup",c),hn(i.wrapper.ownerDocument,"mousemove",d),hn(i.scroller,"dragstart",p),hn(i.scroller,"drop",c),r||(zn(t),a.addNew||Ji(n.doc,e,null,null,a.extend),s&&!b||o&&9==l?setTimeout((function(){i.wrapper.ownerDocument.body.focus({preventScroll:!0}),i.input.focus()}),20):i.input.focus())})),d=function(n){r=r||Math.abs(t.clientX-n.clientX)+Math.abs(t.clientY-n.clientY)>=10},p=function(){return r=!0};s&&(i.scroller.draggable=!0),n.state.draggingText=c,c.copy=!a.moveOnDrag,mn(i.wrapper.ownerDocument,"mouseup",c),mn(i.wrapper.ownerDocument,"mousemove",d),mn(i.scroller,"dragstart",p),mn(i.scroller,"drop",c),n.state.delayingBlurEvent=!0,setTimeout((function(){return i.input.focus()}),20),i.scroller.dragDrop&&i.scroller.dragDrop()}(n,a,t,r):function(n,t,e,a){o&&Ca(n);var i=n.display,r=n.doc;zn(t);var l,s,c=r.sel,d=c.ranges;if(a.addNew&&!a.extend?(s=r.sel.contains(e),l=s>-1?d[s]:new Fi(e,e)):(l=r.sel.primary(),s=r.sel.primIndex),"rectangle"==a.unit)a.addNew||(l=new Fi(e,e)),e=ua(n,t,!0,!0),s=-1;else{var p=yo(n,e,a.unit);l=a.extend?Ki(l,p.anchor,p.head,a.extend):p}a.addNew?-1==s?(s=d.length,ar(r,Mi(n,d.concat([l]),s),{scroll:!1,origin:"*mouse"})):d.length>1&&d[s].empty()&&"char"==a.unit&&!a.extend?(ar(r,Mi(n,d.slice(0,s).concat(d.slice(s+1)),0),{scroll:!1,origin:"*mouse"}),c=r.sel):nr(r,s,l,V):(s=0,ar(r,new Ci([l],0),V),c=r.sel);var u=e;function b(t){if(0!=rt(u,t))if(u=t,"rectangle"==a.unit){for(var i=[],o=n.options.tabSize,d=Y(Qn(r,e.line).text,e.ch,o),p=Y(Qn(r,t.line).text,t.ch,o),b=Math.min(d,p),f=Math.max(d,p),m=Math.min(e.line,t.line),g=Math.min(n.lastLine(),Math.max(e.line,t.line));m<=g;m++){var h=Qn(r,m).text,x=q(h,b,o);b==f?i.push(new Fi(it(m,x),it(m,x))):h.length>x&&i.push(new Fi(it(m,x),it(m,q(h,f,o))))}i.length||i.push(new Fi(e,e)),ar(r,Mi(n,c.ranges.slice(0,s).concat(i),s),{origin:"*mouse",scroll:!1}),n.scrollIntoView(t)}else{var w,k=l,y=yo(n,t,a.unit),v=k.anchor;rt(y.anchor,v)>0?(w=y.head,v=ct(k.from(),y.anchor)):(w=y.anchor,v=st(k.to(),y.head));var z=c.ranges.slice(0);z[s]=function(n,t){var e=t.anchor,a=t.head,i=Qn(n.doc,e.line);if(0==rt(e,a)&&e.sticky==a.sticky)return t;var r=bn(i);if(!r)return t;var o=pn(r,e.ch,e.sticky),l=r[o];if(l.from!=e.ch&&l.to!=e.ch)return t;var s,c=o+(l.from==e.ch==(1!=l.level)?0:1);if(0==c||c==r.length)return t;if(a.line!=e.line)s=(a.line-e.line)*("ltr"==n.doc.direction?1:-1)>0;else{var d=pn(r,a.ch,a.sticky),p=d-o||(a.ch-e.ch)*(1==l.level?-1:1);s=d==c-1||d==c?p<0:p>0}var u=r[c+(s?-1:0)],b=s==(1==u.level),f=b?u.from:u.to,m=b?"after":"before";return e.ch==f&&e.sticky==m?t:new Fi(new it(e.line,f,m),a)}(n,new Fi(pt(r,v),w)),ar(r,Mi(n,z,s),V)}}var f=i.wrapper.getBoundingClientRect(),m=0;function g(t){n.state.selectingText=!1,m=1/0,t&&(zn(t),i.input.focus()),hn(i.wrapper.ownerDocument,"mousemove",h),hn(i.wrapper.ownerDocument,"mouseup",x),r.history.lastSelOrigin=null}var h=ai(n,(function(t){0!==t.buttons&&Mn(t)?function t(e){var o=++m,l=ua(n,e,!0,"rectangle"==a.unit);if(l)if(0!=rt(l,u)){n.curOp.focus=I(L(n)),b(l);var s=ja(i,r);(l.line>=s.to||l.linef.bottom?20:0;c&&setTimeout(ai(n,(function(){m==o&&(i.scroller.scrollTop+=c,t(e))})),50)}}(t):g(t)})),x=ai(n,g);n.state.selectingText=x,mn(i.wrapper.ownerDocument,"mousemove",h),mn(i.wrapper.ownerDocument,"mouseup",x)}(n,a,t,r)}(t,a,r,n):Fn(n)==e.scroller&&zn(n):2==i?(a&&Ji(t.doc,a),setTimeout((function(){return e.input.focus()}),20)):3==i&&(E?t.display.input.onContextMenu(n):Ca(t)))}}function yo(n,t,e){if("char"==e)return new Fi(t,t);if("word"==e)return n.findWordAt(t);if("line"==e)return new Fi(it(t.line,0),pt(n.doc,it(t.line+1,0)));var a=e(n,t);return new Fi(a.from,a.to)}function vo(n,t,e,a){var i,r;if(t.touches)i=t.touches[0].clientX,r=t.touches[0].clientY;else try{i=t.clientX,r=t.clientY}catch(n){return!1}if(i>=Math.floor(n.display.gutters.getBoundingClientRect().right))return!1;a&&zn(t);var o=n.display,l=o.lineDiv.getBoundingClientRect();if(r>l.bottom||!yn(n,e))return _n(t);r-=l.top-o.viewOffset;for(var s=0;s=i)return xn(n,e,n,tt(n.doc,r),n.display.gutterSpecs[s].className,t),_n(t)}}function zo(n,t){return vo(n,t,"gutterClick",!0)}function Eo(n,t){_e(n.display,t)||function(n,t){return!!yn(n,"gutterContextMenu")&&vo(n,t,"gutterContextMenu",!1)}(n,t)||wn(n,t,"contextmenu")||E||n.display.input.onContextMenu(t)}function _o(n){n.display.wrapper.className=n.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+n.options.theme.replace(/(^|\s)\s*/g," cm-s-"),He(n)}wo.prototype.compare=function(n,t,e){return this.time+400>n&&0==rt(t,this.pos)&&e==this.button};var Co={toString:function(){return"CodeMirror.Init"}},Fo={},Mo={};function So(n,t,e){if(!t!=!(e&&e!=Co)){var a=n.display.dragFunctions,i=t?mn:hn;i(n.display.scroller,"dragstart",a.start),i(n.display.scroller,"dragenter",a.enter),i(n.display.scroller,"dragover",a.over),i(n.display.scroller,"dragleave",a.leave),i(n.display.scroller,"drop",a.drop)}}function Do(n){n.options.lineWrapping?(N(n.display.wrapper,"CodeMirror-wrap"),n.display.sizer.style.minWidth="",n.display.sizerWidth=null):(F(n.display.wrapper,"CodeMirror-wrap"),Gt(n)),pa(n),fa(n),He(n),setTimeout((function(){return Wa(n)}),100)}function jo(n,t){var e=this;if(!(this instanceof jo))return new jo(n,t);this.options=t=t?R(t):{},R(Fo,t,!1);var a=t.value;"string"==typeof a?a=new Nr(a,t.mode,null,t.lineSeparator,t.direction):t.mode&&(a.modeOption=t.mode),this.doc=a;var i=new jo.inputStyles[t.inputStyle](this),r=this.display=new ki(n,a,i,t);for(var c in r.wrapper.CodeMirror=this,_o(this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),Xa(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new U,keySeq:null,specialChars:null},t.autofocus&&!x&&r.input.focus(),o&&l<11&&setTimeout((function(){return e.display.input.reset(!0)}),20),function(n){var t=n.display;mn(t.scroller,"mousedown",ai(n,ko)),mn(t.scroller,"dblclick",o&&l<11?ai(n,(function(t){if(!wn(n,t)){var e=ua(n,t);if(e&&!zo(n,t)&&!_e(n.display,t)){zn(t);var a=n.findWordAt(e);Ji(n.doc,a.anchor,a.head)}}})):function(t){return wn(n,t)||zn(t)}),mn(t.scroller,"contextmenu",(function(t){return Eo(n,t)})),mn(t.input.getField(),"contextmenu",(function(e){t.scroller.contains(e.target)||Eo(n,e)}));var e,a={end:0};function i(){t.activeTouch&&(e=setTimeout((function(){return t.activeTouch=null}),1e3),(a=t.activeTouch).end=+new Date)}function r(n,t){if(null==t.left)return!0;var e=t.left-n.left,a=t.top-n.top;return e*e+a*a>400}mn(t.scroller,"touchstart",(function(i){if(!wn(n,i)&&!function(n){if(1!=n.touches.length)return!1;var t=n.touches[0];return t.radiusX<=1&&t.radiusY<=1}(i)&&!zo(n,i)){t.input.ensurePolled(),clearTimeout(e);var r=+new Date;t.activeTouch={start:r,moved:!1,prev:r-a.end<=300?a:null},1==i.touches.length&&(t.activeTouch.left=i.touches[0].pageX,t.activeTouch.top=i.touches[0].pageY)}})),mn(t.scroller,"touchmove",(function(){t.activeTouch&&(t.activeTouch.moved=!0)})),mn(t.scroller,"touchend",(function(e){var a=t.activeTouch;if(a&&!_e(t,e)&&null!=a.left&&!a.moved&&new Date-a.start<300){var o,l=n.coordsChar(t.activeTouch,"page");o=!a.prev||r(a,a.prev)?new Fi(l,l):!a.prev.prev||r(a,a.prev.prev)?n.findWordAt(l):new Fi(it(l.line,0),pt(n.doc,it(l.line+1,0))),n.setSelection(o.anchor,o.head),n.focus(),zn(e)}i()})),mn(t.scroller,"touchcancel",i),mn(t.scroller,"scroll",(function(){t.scroller.clientHeight&&(Pa(n,t.scroller.scrollTop),Ra(n,t.scroller.scrollLeft,!0),xn(n,"scroll",n))})),mn(t.scroller,"mousewheel",(function(t){return _i(n,t)})),mn(t.scroller,"DOMMouseScroll",(function(t){return _i(n,t)})),mn(t.wrapper,"scroll",(function(){return t.wrapper.scrollTop=t.wrapper.scrollLeft=0})),t.dragFunctions={enter:function(t){wn(n,t)||Cn(t)},over:function(t){wn(n,t)||(function(n,t){var e=ua(n,t);if(e){var a=document.createDocumentFragment();ya(n,e,a),n.display.dragCursor||(n.display.dragCursor=D("div",null,"CodeMirror-cursors CodeMirror-dragcursors"),n.display.lineSpace.insertBefore(n.display.dragCursor,n.display.cursorDiv)),S(n.display.dragCursor,a)}}(n,t),Cn(t))},start:function(t){return function(n,t){if(o&&(!n.state.draggingText||+new Date-Ar<100))Cn(t);else if(!wn(n,t)&&!_e(n.display,t)&&(t.dataTransfer.setData("Text",n.getSelection()),t.dataTransfer.effectAllowed="copyMove",t.dataTransfer.setDragImage&&!b)){var e=D("img",null,null,"position: fixed; left: 0; top: 0;");e.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",u&&(e.width=e.height=1,n.display.wrapper.appendChild(e),e._top=e.offsetTop),t.dataTransfer.setDragImage(e,0,0),u&&e.parentNode.removeChild(e)}}(n,t)},drop:ai(n,Tr),leave:function(t){wn(n,t)||Lr(n)}};var s=t.input.getField();mn(s,"keyup",(function(t){return mo.call(n,t)})),mn(s,"keydown",ai(n,fo)),mn(s,"keypress",ai(n,go)),mn(s,"focus",(function(t){return Fa(n,t)})),mn(s,"blur",(function(t){return Ma(n,t)}))}(this),Rr(),Ga(this),this.curOp.forceUpdate=!0,Bi(this,a),t.autofocus&&!x||this.hasFocus()?setTimeout((function(){e.hasFocus()&&!e.state.focused&&Fa(e)}),20):Ma(this),Mo)Mo.hasOwnProperty(c)&&Mo[c](this,t[c],Co);gi(this),t.finishInit&&t.finishInit(this);for(var d=0;d150)){if(!a)return;e="prev"}}else c=0,e="not";"prev"==e?c=t>r.first?Y(Qn(r,t-1).text,null,o):0:"add"==e?c=s+n.options.indentUnit:"subtract"==e?c=s-n.options.indentUnit:"number"==typeof e&&(c=s+e),c=Math.max(0,c);var p="",u=0;if(n.options.indentWithTabs)for(var b=Math.floor(c/o);b;--b)u+=o,p+="\t";if(uo,s=An(t),c=null;if(l&&a.ranges.length>1)if(No&&No.text.join("\n")==t){if(a.ranges.length%No.text.length==0){c=[];for(var d=0;d=0;u--){var b=a.ranges[u],f=b.from(),m=b.to();b.empty()&&(e&&e>0?f=it(f.line,f.ch-e):n.state.overwrite&&!l?m=it(m.line,Math.min(Qn(r,m.line).text.length,m.ch+K(s).length)):l&&No&&No.lineWise&&No.text.join("\n")==s.join("\n")&&(f=m=it(f.line,0)));var g={from:f,to:m,text:c?c[u%c.length]:s,origin:i||(l?"paste":n.state.cutIncoming>o?"cut":"+input")};br(n.doc,g),pe(n,"inputRead",n,g)}t&&!l&&Po(n,t),Na(n),n.curOp.updateInput<2&&(n.curOp.updateInput=p),n.curOp.typing=!0,n.state.pasteIncoming=n.state.cutIncoming=-1}function Lo(n,t){var e=n.clipboardData&&n.clipboardData.getData("Text");if(e)return n.preventDefault(),t.isReadOnly()||t.options.disableInput||!t.hasFocus()||ei(t,(function(){return To(t,e,0,null,"paste")})),!0}function Po(n,t){if(n.options.electricChars&&n.options.smartIndent)for(var e=n.doc.sel,a=e.ranges.length-1;a>=0;a--){var i=e.ranges[a];if(!(i.head.ch>100||a&&e.ranges[a-1].head.line==i.head.line)){var r=n.getModeAt(i.head),o=!1;if(r.electricChars){for(var l=0;l-1){o=Io(n,i.head.line,"smart");break}}else r.electricInput&&r.electricInput.test(Qn(n.doc,i.head.line).text.slice(0,i.head.ch))&&(o=Io(n,i.head.line,"smart"));o&&pe(n,"electricInput",n,i.head.line)}}}function Bo(n){for(var t=[],e=[],a=0;a0?0:-1));if(isNaN(d))o=null;else{var p=e>0?d>=55296&&d<56320:d>=56320&&d<57343;o=new it(t.line,Math.max(0,Math.min(l.text.length,t.ch+e*(p?2:1))),-e)}}else o=i?function(n,t,e,a){var i=bn(t,n.doc.direction);if(!i)return eo(t,e,a);e.ch>=t.text.length?(e.ch=t.text.length,e.sticky="before"):e.ch<=0&&(e.ch=0,e.sticky="after");var r=pn(i,e.ch,e.sticky),o=i[r];if("ltr"==n.doc.direction&&o.level%2==0&&(a>0?o.to>e.ch:o.from=o.from&&u>=d.begin)){var b=p?"before":"after";return new it(e.line,u,b)}}var f=function(n,t,a){for(var r=function(n,t){return t?new it(e.line,s(n,1),"before"):new it(e.line,n,"after")};n>=0&&n0==(1!=o.level),c=l?a.begin:s(a.end,-1);if(o.from<=c&&c0?d.end:s(d.begin,-1);return null==g||a>0&&g==t.text.length||!(m=f(a>0?0:i.length-1,a,c(g)))?null:m}(n.cm,l,t,e):eo(l,t,e);if(null==o){if(r||(c=t.line+s)=n.first+n.size||(t=new it(c,t.ch,t.sticky),!(l=Qn(n,c))))return!1;t=ao(i,n.cm,l,t.line,s)}else t=o;return!0}if("char"==a||"codepoint"==a)c();else if("column"==a)c(!0);else if("word"==a||"group"==a)for(var d=null,p="group"==a,u=n.cm&&n.cm.getHelper(t,"wordChars"),b=!0;!(e<0)||c(!b);b=!1){var f=l.text.charAt(t.ch)||"\n",m=an(f,u)?"w":p&&"\n"==f?"n":!p||/\s/.test(f)?null:"p";if(!p||b||m||(m="s"),d&&d!=m){e<0&&(e=1,c(),t.sticky="after");break}if(m&&(d=m),e>0&&!c(!b))break}var g=cr(n,t,r,o,!0);return ot(r,g)&&(g.hitSide=!0),g}function Ho(n,t,e,a){var i,r,o=n.doc,l=t.left;if("page"==a){var s=Math.min(n.display.wrapper.clientHeight,P(n).innerHeight||o(n).documentElement.clientHeight),c=Math.max(s-.5*oa(n.display),3);i=(e>0?t.bottom:t.top)+e*c}else"line"==a&&(i=e>0?t.bottom+3:t.top-3);for(;(r=$e(n,l,i)).outside;){if(e<0?i<=0:i>=o.height){r.hitSide=!0;break}i+=5*e}return r}var Wo=function(n){this.cm=n,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new U,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null};function Zo(n,t){var e=Ne(n,t.line);if(!e||e.hidden)return null;var a=Qn(n.doc,t.line),i=Oe(e,a,t.line),r=bn(a,n.doc.direction),o="left";r&&(o=pn(r,t.ch)%2?"right":"left");var l=Be(i.map,t.ch,o);return l.offset="right"==l.collapse?l.end:l.start,l}function Vo(n,t){return t&&(n.bad=!0),n}function Xo(n,t,e){var a;if(t==n.display.lineDiv){if(!(a=n.display.lineDiv.childNodes[e]))return Vo(n.clipPos(it(n.display.viewTo-1)),!0);t=null,e=0}else for(a=t;;a=a.parentNode){if(!a||a==n.display.lineDiv)return null;if(a.parentNode&&a.parentNode==n.display.lineDiv)break}for(var i=0;i=t.display.viewTo||r.line=t.display.viewFrom&&Zo(t,i)||{node:s[0].measure.map[2],offset:0},d=r.linea.firstLine()&&(o=it(o.line-1,Qn(a.doc,o.line-1).length)),l.ch==Qn(a.doc,l.line).text.length&&l.linei.viewTo-1)return!1;o.line==i.viewFrom||0==(n=ba(a,o.line))?(t=nt(i.view[0].line),e=i.view[0].node):(t=nt(i.view[n].line),e=i.view[n-1].node.nextSibling);var s,c,d=ba(a,l.line);if(d==i.view.length-1?(s=i.viewTo-1,c=i.lineDiv.lastChild):(s=nt(i.view[d+1].line)-1,c=i.view[d+1].node.previousSibling),!e)return!1;for(var p=a.doc.splitLines(function(n,t,e,a,i){var r="",o=!1,l=n.doc.lineSeparator(),s=!1;function c(){o&&(r+=l,s&&(r+=l),o=s=!1)}function d(n){n&&(c(),r+=n)}function p(t){if(1==t.nodeType){var e=t.getAttribute("cm-text");if(e)return void d(e);var r,u=t.getAttribute("cm-marker");if(u){var b=n.findMarks(it(a,0),it(i+1,0),(g=+u,function(n){return n.id==g}));return void(b.length&&(r=b[0].find(0))&&d(Kn(n.doc,r.from,r.to).join(l)))}if("false"==t.getAttribute("contenteditable"))return;var f=/^(pre|div|p|li|table|br)$/i.test(t.nodeName);if(!/^br$/i.test(t.nodeName)&&0==t.textContent.length)return;f&&c();for(var m=0;m1&&u.length>1;)if(K(p)==K(u))p.pop(),u.pop(),s--;else{if(p[0]!=u[0])break;p.shift(),u.shift(),t++}for(var b=0,f=0,m=p[0],g=u[0],h=Math.min(m.length,g.length);bo.ch&&x.charCodeAt(x.length-f-1)==w.charCodeAt(w.length-f-1);)b--,f++;p[p.length-1]=x.slice(0,x.length-f).replace(/^\u200b+/,""),p[0]=p[0].slice(b).replace(/\u200b+$/,"");var y=it(t,b),v=it(s,u.length?K(u).length-f:0);return p.length>1||p[0]||rt(y,v)?(xr(a.doc,p,y,v,"+input"),!0):void 0},Wo.prototype.ensurePolled=function(){this.forceCompositionEnd()},Wo.prototype.reset=function(){this.forceCompositionEnd()},Wo.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},Wo.prototype.readFromDOMSoon=function(){var n=this;null==this.readDOMTimeout&&(this.readDOMTimeout=setTimeout((function(){if(n.readDOMTimeout=null,n.composing){if(!n.composing.done)return;n.composing=null}n.updateFromDOM()}),80))},Wo.prototype.updateFromDOM=function(){var n=this;!this.cm.isReadOnly()&&this.pollContent()||ei(this.cm,(function(){return fa(n.cm)}))},Wo.prototype.setUneditable=function(n){n.contentEditable="false"},Wo.prototype.onKeyPress=function(n){0==n.charCode||this.composing||(n.preventDefault(),this.cm.isReadOnly()||ai(this.cm,To)(this.cm,String.fromCharCode(null==n.charCode?n.keyCode:n.charCode),0))},Wo.prototype.readOnlyChanged=function(n){this.div.contentEditable=String("nocursor"!=n)},Wo.prototype.onContextMenu=function(){},Wo.prototype.resetPosition=function(){},Wo.prototype.needsContentAttribute=!0;var Go=function(n){this.cm=n,this.prevInput="",this.pollingFast=!1,this.polling=new U,this.hasSelection=!1,this.composing=null,this.resetting=!1};Go.prototype.init=function(n){var t=this,e=this,a=this.cm;this.createField(n);var i=this.textarea;function r(n){if(!wn(a,n)){if(a.somethingSelected())Ao({lineWise:!1,text:a.getSelections()});else{if(!a.options.lineWiseCopyCut)return;var t=Bo(a);Ao({lineWise:!0,text:t.text}),"cut"==n.type?a.setSelections(t.ranges,null,Z):(e.prevInput="",i.value=t.text.join("\n"),T(i))}"cut"==n.type&&(a.state.cutIncoming=+new Date)}}n.wrapper.insertBefore(this.wrapper,n.wrapper.firstChild),g&&(i.style.width="0px"),mn(i,"input",(function(){o&&l>=9&&t.hasSelection&&(t.hasSelection=null),e.poll()})),mn(i,"paste",(function(n){wn(a,n)||Lo(n,a)||(a.state.pasteIncoming=+new Date,e.fastPoll())})),mn(i,"cut",r),mn(i,"copy",r),mn(n.scroller,"paste",(function(t){if(!_e(n,t)&&!wn(a,t)){if(!i.dispatchEvent)return a.state.pasteIncoming=+new Date,void e.focus();var r=new Event("paste");r.clipboardData=t.clipboardData,i.dispatchEvent(r)}})),mn(n.lineSpace,"selectstart",(function(t){_e(n,t)||zn(t)})),mn(i,"compositionstart",(function(){var n=a.getCursor("from");e.composing&&e.composing.range.clear(),e.composing={start:n,range:a.markText(n,a.getCursor("to"),{className:"CodeMirror-composing"})}})),mn(i,"compositionend",(function(){e.composing&&(e.poll(),e.composing.range.clear(),e.composing=null)}))},Go.prototype.createField=function(n){this.wrapper=Yo(),this.textarea=this.wrapper.firstChild},Go.prototype.screenReaderLabelChanged=function(n){n?this.textarea.setAttribute("aria-label",n):this.textarea.removeAttribute("aria-label")},Go.prototype.prepareSelection=function(){var n=this.cm,t=n.display,e=n.doc,a=ka(n);if(n.options.moveInputWithCursor){var i=Qe(n,e.sel.primary().head,"div"),r=t.wrapper.getBoundingClientRect(),o=t.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+o.top-r.top)),a.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+o.left-r.left))}return a},Go.prototype.showSelection=function(n){var t=this.cm.display;S(t.cursorDiv,n.cursors),S(t.selectionDiv,n.selection),null!=n.teTop&&(this.wrapper.style.top=n.teTop+"px",this.wrapper.style.left=n.teLeft+"px")},Go.prototype.reset=function(n){if(!(this.contextMenuPending||this.composing&&n)){var t=this.cm;if(this.resetting=!0,t.somethingSelected()){this.prevInput="";var e=t.getSelection();this.textarea.value=e,t.state.focused&&T(this.textarea),o&&l>=9&&(this.hasSelection=e)}else n||(this.prevInput=this.textarea.value="",o&&l>=9&&(this.hasSelection=null));this.resetting=!1}},Go.prototype.getField=function(){return this.textarea},Go.prototype.supportsTouch=function(){return!1},Go.prototype.focus=function(){if("nocursor"!=this.cm.options.readOnly&&(!x||I(this.textarea.ownerDocument)!=this.textarea))try{this.textarea.focus()}catch(n){}},Go.prototype.blur=function(){this.textarea.blur()},Go.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},Go.prototype.receivedFocus=function(){this.slowPoll()},Go.prototype.slowPoll=function(){var n=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,(function(){n.poll(),n.cm.state.focused&&n.slowPoll()}))},Go.prototype.fastPoll=function(){var n=!1,t=this;t.pollingFast=!0,t.polling.set(20,(function e(){t.poll()||n?(t.pollingFast=!1,t.slowPoll()):(n=!0,t.polling.set(60,e))}))},Go.prototype.poll=function(){var n=this,t=this.cm,e=this.textarea,a=this.prevInput;if(this.contextMenuPending||this.resetting||!t.state.focused||Tn(e)&&!a&&!this.composing||t.isReadOnly()||t.options.disableInput||t.state.keySeq)return!1;var i=e.value;if(i==a&&!t.somethingSelected())return!1;if(o&&l>=9&&this.hasSelection===i||w&&/[\uf700-\uf7ff]/.test(i))return t.display.input.reset(),!1;if(t.doc.sel==t.display.selForContextMenu){var r=i.charCodeAt(0);if(8203!=r||a||(a="​"),8666==r)return this.reset(),this.cm.execCommand("undo")}for(var s=0,c=Math.min(a.length,i.length);s1e3||i.indexOf("\n")>-1?e.value=n.prevInput="":n.prevInput=i,n.composing&&(n.composing.range.clear(),n.composing.range=t.markText(n.composing.start,t.getCursor("to"),{className:"CodeMirror-composing"}))})),!0},Go.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},Go.prototype.onKeyPress=function(){o&&l>=9&&(this.hasSelection=null),this.fastPoll()},Go.prototype.onContextMenu=function(n){var t=this,e=t.cm,a=e.display,i=t.textarea;t.contextMenuPending&&t.contextMenuPending();var r=ua(e,n),c=a.scroller.scrollTop;if(r&&!u){e.options.resetSelectionOnContextMenu&&-1==e.doc.sel.contains(r)&&ai(e,ar)(e.doc,Si(r),Z);var d,p=i.style.cssText,b=t.wrapper.style.cssText,f=t.wrapper.offsetParent.getBoundingClientRect();if(t.wrapper.style.cssText="position: static",i.style.cssText="position: absolute; width: 30px; height: 30px;\n top: "+(n.clientY-f.top-5)+"px; left: "+(n.clientX-f.left-5)+"px;\n z-index: 1000; background: "+(o?"rgba(255, 255, 255, .05)":"transparent")+";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);",s&&(d=i.ownerDocument.defaultView.scrollY),a.input.focus(),s&&i.ownerDocument.defaultView.scrollTo(null,d),a.input.reset(),e.somethingSelected()||(i.value=t.prevInput=" "),t.contextMenuPending=h,a.selForContextMenu=e.doc.sel,clearTimeout(a.detectingSelectAll),o&&l>=9&&g(),E){Cn(n);var m=function(){hn(window,"mouseup",m),setTimeout(h,20)};mn(window,"mouseup",m)}else setTimeout(h,50)}function g(){if(null!=i.selectionStart){var n=e.somethingSelected(),r="​"+(n?i.value:"");i.value="⇚",i.value=r,t.prevInput=n?"":"​",i.selectionStart=1,i.selectionEnd=r.length,a.selForContextMenu=e.doc.sel}}function h(){if(t.contextMenuPending==h&&(t.contextMenuPending=!1,t.wrapper.style.cssText=b,i.style.cssText=p,o&&l<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=c),null!=i.selectionStart)){(!o||o&&l<9)&&g();var n=0,r=function(){a.selForContextMenu==e.doc.sel&&0==i.selectionStart&&i.selectionEnd>0&&"​"==t.prevInput?ai(e,pr)(e):n++<10?a.detectingSelectAll=setTimeout(r,500):(a.selForContextMenu=null,a.input.reset())};a.detectingSelectAll=setTimeout(r,200)}}},Go.prototype.readOnlyChanged=function(n){n||this.reset(),this.textarea.disabled="nocursor"==n,this.textarea.readOnly=!!n},Go.prototype.setUneditable=function(){},Go.prototype.needsContentAttribute=!1,function(n){var t=n.optionHandlers;function e(e,a,i,r){n.defaults[e]=a,i&&(t[e]=r?function(n,t,e){e!=Co&&i(n,t,e)}:i)}n.defineOption=e,n.Init=Co,e("value","",(function(n,t){return n.setValue(t)}),!0),e("mode",null,(function(n,t){n.doc.modeOption=t,Ni(n)}),!0),e("indentUnit",2,Ni,!0),e("indentWithTabs",!1),e("smartIndent",!0),e("tabSize",4,(function(n){Ai(n),He(n),fa(n)}),!0),e("lineSeparator",null,(function(n,t){if(n.doc.lineSep=t,t){var e=[],a=n.doc.first;n.doc.iter((function(n){for(var i=0;;){var r=n.text.indexOf(t,i);if(-1==r)break;i=r+t.length,e.push(it(a,r))}a++}));for(var i=e.length-1;i>=0;i--)xr(n.doc,t,e[i],it(e[i].line,e[i].ch+t.length))}})),e("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/g,(function(n,t,e){n.state.specialChars=new RegExp(t.source+(t.test("\t")?"":"|\t"),"g"),e!=Co&&n.refresh()})),e("specialCharPlaceholder",ee,(function(n){return n.refresh()}),!0),e("electricChars",!0),e("inputStyle",x?"contenteditable":"textarea",(function(){throw new Error("inputStyle can not (yet) be changed in a running editor")}),!0),e("spellcheck",!1,(function(n,t){return n.getInputField().spellcheck=t}),!0),e("autocorrect",!1,(function(n,t){return n.getInputField().autocorrect=t}),!0),e("autocapitalize",!1,(function(n,t){return n.getInputField().autocapitalize=t}),!0),e("rtlMoveVisually",!y),e("wholeLineUpdateBefore",!0),e("theme","default",(function(n){_o(n),wi(n)}),!0),e("keyMap","default",(function(n,t,e){var a=$r(t),i=e!=Co&&$r(e);i&&i.detach&&i.detach(n,a),a.attach&&a.attach(n,i||null)})),e("extraKeys",null),e("configureMouse",null),e("lineWrapping",!1,Do,!0),e("gutters",[],(function(n,t){n.display.gutterSpecs=hi(t,n.options.lineNumbers),wi(n)}),!0),e("fixedGutter",!0,(function(n,t){n.display.gutters.style.left=t?ca(n.display)+"px":"0",n.refresh()}),!0),e("coverGutterNextToScrollbar",!1,(function(n){return Wa(n)}),!0),e("scrollbarStyle","native",(function(n){Xa(n),Wa(n),n.display.scrollbars.setScrollTop(n.doc.scrollTop),n.display.scrollbars.setScrollLeft(n.doc.scrollLeft)}),!0),e("lineNumbers",!1,(function(n,t){n.display.gutterSpecs=hi(n.options.gutters,t),wi(n)}),!0),e("firstLineNumber",1,wi,!0),e("lineNumberFormatter",(function(n){return n}),wi,!0),e("showCursorWhenSelecting",!1,wa,!0),e("resetSelectionOnContextMenu",!0),e("lineWiseCopyCut",!0),e("pasteLinesPerSelection",!0),e("selectionsMayTouch",!1),e("readOnly",!1,(function(n,t){"nocursor"==t&&(Ma(n),n.display.input.blur()),n.display.input.readOnlyChanged(t)})),e("screenReaderLabel",null,(function(n,t){t=""===t?null:t,n.display.input.screenReaderLabelChanged(t)})),e("disableInput",!1,(function(n,t){t||n.display.input.reset()}),!0),e("dragDrop",!0,So),e("allowDropFileTypes",null),e("cursorBlinkRate",530),e("cursorScrollMargin",0),e("cursorHeight",1,wa,!0),e("singleCursorHeightPerLine",!0,wa,!0),e("workTime",100),e("workDelay",100),e("flattenSpans",!0,Ai,!0),e("addModeClass",!1,Ai,!0),e("pollInterval",100),e("undoDepth",200,(function(n,t){return n.doc.history.undoDepth=t})),e("historyEventDelay",1250),e("viewportMargin",10,(function(n){return n.refresh()}),!0),e("maxHighlightLength",1e4,Ai,!0),e("moveInputWithCursor",!0,(function(n,t){t||n.display.input.resetPosition()})),e("tabindex",null,(function(n,t){return n.display.input.getField().tabIndex=t||""})),e("autofocus",null),e("direction","ltr",(function(n,t){return n.doc.setDirection(t)}),!0),e("phrases",null)}(jo),function(n){var t=n.optionHandlers,e=n.helpers={};n.prototype={constructor:n,focus:function(){P(this).focus(),this.display.input.focus()},setOption:function(n,e){var a=this.options,i=a[n];a[n]==e&&"mode"!=n||(a[n]=e,t.hasOwnProperty(n)&&ai(this,t[n])(this,e,i),xn(this,"optionChange",this,n))},getOption:function(n){return this.options[n]},getDoc:function(){return this.doc},addKeyMap:function(n,t){this.state.keyMaps[t?"push":"unshift"]($r(n))},removeKeyMap:function(n){for(var t=this.state.keyMaps,e=0;ee&&(Io(this,i.head.line,n,!0),e=i.head.line,a==this.doc.sel.primIndex&&Na(this));else{var r=i.from(),o=i.to(),l=Math.max(e,r.line);e=Math.min(this.lastLine(),o.line-(o.ch?0:1))+1;for(var s=l;s0&&nr(this.doc,a,new Fi(r,c[a].to()),Z)}}})),getTokenAt:function(n,t){return vt(this,n,t)},getLineTokens:function(n,t){return vt(this,it(n),t,!0)},getTokenTypeAt:function(n){n=pt(this.doc,n);var t,e=gt(this,Qn(this.doc,n.line)),a=0,i=(e.length-1)/2,r=n.ch;if(0==r)t=e[2];else for(;;){var o=a+i>>1;if((o?e[2*o-1]:0)>=r)i=o;else{if(!(e[2*o+1]r&&(n=r,i=!0),a=Qn(this.doc,n)}else a=n;return Xe(this,a,{top:0,left:0},t||"page",e||i).top+(i?this.doc.height-Xt(a):0)},defaultTextHeight:function(){return oa(this.display)},defaultCharWidth:function(){return la(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(n,t,e,a,i){var r,o,l,s=this.display,c=(n=Qe(this,pt(this.doc,n))).bottom,d=n.left;if(t.style.position="absolute",t.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(t),s.sizer.appendChild(t),"over"==a)c=n.top;else if("above"==a||"near"==a){var p=Math.max(s.wrapper.clientHeight,this.doc.height),u=Math.max(s.sizer.clientWidth,s.lineSpace.clientWidth);("above"==a||n.bottom+t.offsetHeight>p)&&n.top>t.offsetHeight?c=n.top-t.offsetHeight:n.bottom+t.offsetHeight<=p&&(c=n.bottom),d+t.offsetWidth>u&&(d=u-t.offsetWidth)}t.style.top=c+"px",t.style.left=t.style.right="","right"==i?(d=s.sizer.clientWidth-t.offsetWidth,t.style.right="0px"):("left"==i?d=0:"middle"==i&&(d=(s.sizer.clientWidth-t.offsetWidth)/2),t.style.left=d+"px"),e&&(r=this,o={left:d,top:c,right:d+t.offsetWidth,bottom:c+t.offsetHeight},null!=(l=Oa(r,o)).scrollTop&&Pa(r,l.scrollTop),null!=l.scrollLeft&&Ra(r,l.scrollLeft))},triggerOnKeyDown:ii(fo),triggerOnKeyPress:ii(go),triggerOnKeyUp:mo,triggerOnMouseDown:ii(ko),execCommand:function(n){if(io.hasOwnProperty(n))return io[n].call(null,this)},triggerElectric:ii((function(n){Po(this,n)})),findPosH:function(n,t,e,a){var i=1;t<0&&(i=-1,t=-t);for(var r=pt(this.doc,n),o=0;o0&&o(t.charAt(e-1));)--e;for(;a.5||this.options.lineWrapping)&&pa(this),xn(this,"refresh",this)})),swapDoc:ii((function(n){var t=this.doc;return t.cm=null,this.state.selectingText&&this.state.selectingText(),Bi(this,n),He(this),this.display.input.reset(),Aa(this,n.scrollLeft,n.scrollTop),this.curOp.forceScroll=!0,pe(this,"swapDoc",this,t),t})),phrase:function(n){var t=this.options.phrases;return t&&Object.prototype.hasOwnProperty.call(t,n)?t[n]:n},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},vn(n),n.registerHelper=function(t,a,i){e.hasOwnProperty(t)||(e[t]=n[t]={_global:[]}),e[t][a]=i},n.registerGlobalHelper=function(t,a,i,r){n.registerHelper(t,a,r),e[t]._global.push({pred:i,val:r})}}(jo);var Qo="iter insert remove copy getEditor constructor".split(" ");for(var Ko in Nr.prototype)Nr.prototype.hasOwnProperty(Ko)&&H(Qo,Ko)<0&&(jo.prototype[Ko]=function(n){return function(){return n.apply(this.doc,arguments)}}(Nr.prototype[Ko]));return vn(Nr),jo.inputStyles={textarea:Go,contenteditable:Wo},jo.defineMode=function(n){jo.defaults.mode||"null"==n||(jo.defaults.mode=n),Yn.apply(this,arguments)},jo.defineMIME=function(n,t){Rn[n]=t},jo.defineMode("null",(function(){return{token:function(n){return n.skipToEnd()}}})),jo.defineMIME("text/plain","null"),jo.defineExtension=function(n,t){jo.prototype[n]=t},jo.defineDocExtension=function(n,t){Nr.prototype[n]=t},jo.fromTextArea=function(n,t){if((t=t?R(t):{}).value=n.value,!t.tabindex&&n.tabIndex&&(t.tabindex=n.tabIndex),!t.placeholder&&n.placeholder&&(t.placeholder=n.placeholder),null==t.autofocus){var e=I(n.ownerDocument);t.autofocus=e==n||null!=n.getAttribute("autofocus")&&e==document.body}function a(){n.value=l.getValue()}var i;if(n.form&&(mn(n.form,"submit",a),!t.leaveSubmitMethodAlone)){var r=n.form;i=r.submit;try{var o=r.submit=function(){a(),r.submit=i,r.submit(),r.submit=o}}catch(n){}}t.finishInit=function(e){e.save=a,e.getTextArea=function(){return n},e.toTextArea=function(){e.toTextArea=isNaN,a(),n.parentNode.removeChild(e.getWrapperElement()),n.style.display="",n.form&&(hn(n.form,"submit",a),t.leaveSubmitMethodAlone||"function"!=typeof n.form.submit||(n.form.submit=i))}},n.style.display="none";var l=jo((function(t){return n.parentNode.insertBefore(t,n.nextSibling)}),t);return l},function(n){n.off=hn,n.on=mn,n.wheelEventPixels=Ei,n.Doc=Nr,n.splitLines=An,n.countColumn=Y,n.findColumn=q,n.isWordChar=en,n.Pass=W,n.signal=xn,n.Line=Qt,n.changeEnd=Di,n.scrollbarModel=Va,n.Pos=it,n.cmpPos=rt,n.modes=Bn,n.mimeModes=Rn,n.resolveMode=Un,n.getMode=Hn,n.modeExtensions=Wn,n.extendMode=Zn,n.copyState=Vn,n.startState=qn,n.innerMode=Xn,n.commands=io,n.keyMap=Vr,n.keyName=Jr,n.isModifierKey=Qr,n.lookupKey=Gr,n.normalizeKeyMap=qr,n.StringStream=Gn,n.SharedTextMarker=Dr,n.TextMarker=Mr,n.LineWidget=_r,n.e_preventDefault=zn,n.e_stopPropagation=En,n.e_stop=Cn,n.addClass=N,n.contains=O,n.rmClass=F,n.keyNames=Ur}(jo),jo.version="5.65.10",jo}()},function(n,t,e){!function(n){"use strict";n.defineMode("shell",(function(){var t={};function e(n,e){for(var a=0;a1&&n.eat("$");var e=n.next();return/['"({]/.test(e)?(t.tokens[0]=l(e,"("==e?"quote":"{"==e?"def":"string"),d(n,t)):(/\d/.test(e)||n.eatWhile(/\w/),t.tokens.shift(),"def")};function d(n,t){return(t.tokens[0]||o)(n,t)}return{startState:function(){return{tokens:[]}},token:function(n,t){return d(n,t)},closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}})),n.defineMIME("text/x-sh","shell"),n.defineMIME("application/x-sh","shell")}(e(75))},function(n,t,e){var a=e(294),i=e(3);"string"==typeof a&&(a=[[n.i,a,""]]),n.exports=a.locals||{},n.exports._getContent=function(){return a},n.exports._getCss=function(){return a.toString()},n.exports._insertCss=function(n){return i(a,n)}},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var i=Object.getOwnPropertyDescriptor(t,e);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,i)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),i=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),r=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return i(t,n),t},o=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=r(e(0)),s=(r(e(79)),e(80),e(10)),c=e(23),d=e(7),p=o(e(81)),u=o(e(304)),b=e(305),f=o(e(43));e(309),e(311);t.default=function(n,t){var e=n.state.url,a=(0,c.matchRoutes)(f.default,e).map((function(n){var t=n.route,e=t.component&&t.component.fetch;return e instanceof Function?e():Promise.resolve(null)}));return Promise.all(a).then((function(t){var a=n.state;t.forEach((function(n){Object.assign(a,n)})),n.state=Object.assign({},n.state,a);var i=(0,b.create)(a);return function(){return l.createElement(u.default,null,l.createElement("div",{style:{height:"100%"}},l.createElement(s.Provider,{store:i},l.createElement(d.StaticRouter,{location:e,context:{}},l.createElement(p.default,null)))))}}))}},function(n,t){n.exports=require("react-dom")},function(n,t){n.exports=require("react-hot-loader")},function(n,t,e){"use strict";var a=this&&this.__createBinding||(Object.create?function(n,t,e,a){void 0===a&&(a=e);var i=Object.getOwnPropertyDescriptor(t,e);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[e]}}),Object.defineProperty(n,a,i)}:function(n,t,e,a){void 0===a&&(a=e),n[a]=t[e]}),i=this&&this.__setModuleDefault||(Object.create?function(n,t){Object.defineProperty(n,"default",{enumerable:!0,value:t})}:function(n,t){n.default=t}),r=this&&this.__importStar||function(n){if(n&&n.__esModule)return n;var t={};if(null!=n)for(var e in n)"default"!==e&&Object.prototype.hasOwnProperty.call(n,e)&&a(t,n,e);return i(t,n),t},o=this&&this.__importDefault||function(n){return n&&n.__esModule?n:{default:n}};Object.defineProperty(t,"__esModule",{value:!0});var l=r(e(0)),s=e(10),c=e(23),d=e(7),p=e(1),u=o(e(82)),b=e(24),f=o(e(43)),m=r(e(46)),g=e(297);e(298),e(300),e(302);t.default=function(){var n=(0,b.bindActionCreators)(m,(0,s.useDispatch)()).changeLocalIp;return(0,l.useEffect)((function(){var t,e,a,i;window.console.log("%cApp current version: v".concat(g.version),"font-family: Cabin, Helvetica, Arial, sans-serif;text-align: left;font-size:32px;color:#B21212;"),t=window,e=document,t.hj=t.hj||function(){(t.hj.q=t.hj.q||[]).push(arguments)},t._hjSettings={hjid:2133522,hjsv:6},a=e.getElementsByTagName("head")[0],(i=e.createElement("script")).async=1,i.src="https://static.hotjar.com/c/hotjar-"+t._hjSettings.hjid+".js?sv="+t._hjSettings.hjsv,a.appendChild(i),n()}),[]),l.default.createElement("div",{style:{height:"100%"}},l.default.createElement(p.ConfigProvider,{locale:u.default},l.default.createElement(d.Switch,null,(0,c.renderRoutes)(f.default))))}},function(n,t,e){"use strict";var a=e(16);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=a(e(83)).default;t.default=i},function(n,t,e){"use strict";var a=e(16);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var i=a(e(84)),r=a(e(41)),o=a(e(42)),l=a(e(87)),s="${label}不是一个有效的${type}",c={locale:"zh-cn",Pagination:i.default,DatePicker:r.default,TimePicker:o.default,Calendar:l.default,global:{placeholder:"请选择"},Table:{filterTitle:"筛选",filterConfirm:"确定",filterReset:"重置",filterEmptyText:"无筛选项",selectAll:"全选当页",selectInvert:"反选当页",selectNone:"清空所有",selectionAll:"全选所有",sortTitle:"排序",expand:"展开行",collapse:"关闭行",triggerDesc:"点击降序",triggerAsc:"点击升序",cancelSort:"取消排序"},Modal:{okText:"确定",cancelText:"取消",justOkText:"知道了"},Popconfirm:{cancelText:"取消",okText:"确定"},Transfer:{searchPlaceholder:"请输入搜索内容",itemUnit:"项",itemsUnit:"项",remove:"删除",selectCurrent:"全选当页",removeCurrent:"删除当页",selectAll:"全选所有",removeAll:"删除全部",selectInvert:"反选当页"},Upload:{uploading:"文件上传中",removeFile:"删除文件",uploadError:"上传错误",previewFile:"预览文件",downloadFile:"下载文件"},Empty:{description:"暂无数据"},Icon:{icon:"图标"},Text:{edit:"编辑",copy:"复制",copied:"复制成功",expand:"展开"},PageHeader:{back:"返回"},Form:{optional:"(可选)",defaultValidateMessages:{default:"字段验证错误${label}",required:"请输入${label}",enum:"${label}必须是其中一个[${enum}]",whitespace:"${label}不能为空字符",date:{format:"${label}日期格式无效",parse:"${label}不能转换为日期",invalid:"${label}是一个无效日期"},types:{string:s,method:s,array:s,object:s,number:s,date:s,boolean:s,integer:s,float:s,regexp:s,email:s,url:s,hex:s},string:{len:"${label}须为${len}个字符",min:"${label}最少${min}个字符",max:"${label}最多${max}个字符",range:"${label}须在${min}-${max}字符之间"},number:{len:"${label}必须等于${len}",min:"${label}最小值为${min}",max:"${label}最大值为${max}",range:"${label}须在${min}-${max}之间"},array:{len:"须为${len}个${label}",min:"最少${min}个${label}",max:"最多${max}个${label}",range:"${label}数量须在${min}-${max}之间"},pattern:{mismatch:"${label}与模式不匹配${pattern}"}}},Image:{preview:"预览"}};t.default=c},function(n,t,e){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;t.default={items_per_page:"条/页",jump_to:"跳至",jump_to_confirm:"确定",page:"页",prev_page:"上一页",next_page:"下一页",prev_5:"向前 5 页",next_5:"向后 5 页",prev_3:"向前 3 页",next_3:"向后 3 页",page_size:"页码"}},function(n,t){function e(){return n.exports=e=Object.assign?Object.assign.bind():function(n){for(var t=1;t-1}))}));n.length&&w([n[0].path])}),[i]),l.default.createElement(h,{className:"dt-layout-header header_component"},l.default.createElement("div",{className:"dt-header-log-wrapper logo"},l.default.createElement(c.Link,{to:"/page/toolbox"},l.default.createElement("img",{className:"logo_img",src:b.default}),l.default.createElement("span",{className:"system-title"},"Doraemon"))),l.default.createElement("div",{className:"menu_content"},l.default.createElement(p.Menu,{mode:"horizontal",theme:"dark",onClick:function(n){w(n.key)},selectedKeys:o},x.map((function(n){var t=n.children,e=n.name,a=n.path,i=n.icon;return Array.isArray(t)&&t.length>0?l.default.createElement(g,{key:e,title:l.default.createElement("span",null,i,l.default.createElement("span",null,"Navigation Two"))},t.map((function(n){return l.default.createElement(p.Menu.Item,{key:n.path},l.default.createElement(c.Link,{to:n.path},n.icon,l.default.createElement("span",null,n.name)))}))):l.default.createElement(p.Menu.Item,{key:a},l.default.createElement(c.Link,{to:a},i,l.default.createElement("span",null,e)))}))),l.default.createElement("div",null,l.default.createElement("a",{href:(null===m.default||void 0===m.default?void 0:m.default.helpDocUrl)||"",rel:"noopener noreferrer",target:"_blank"},l.default.createElement(d.QuestionCircleOutlined,{className:"help-link"})),l.default.createElement("span",{className:"local-ip ml-20"},"本机IP: ".concat(a)),l.default.createElement(d.SyncOutlined,{className:"refresh-cion",onClick:function(){return k(!0)}}))))}},function(n,t,e){n.exports=e.p+"img/logo.ff9eed58.png"},function(n,t,e){var a=e(92),i=e(3);"string"==typeof a&&(a=[[n.i,a,""]]),n.exports=a.locals||{},n.exports._getContent=function(){return a},n.exports._getCss=function(){return a.toString()},n.exports._insertCss=function(n){return i(a,n)}},function(n,t,e){(t=e(2)(!1)).push([n.i,".header_component .menu_content{-webkit-box-flex:1;-webkit-flex:1;flex:1;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:0 20px}.header_component .menu_content .ant-menu{overflow:hidden;font-size:14px}.header_component .help-link{color:#fff;font-size:18px;vertical-align:sub;cursor:pointer}.header_component .refresh-cion{color:#fff;font-size:16px;margin-left:10px}",""]),n.exports=t},function(n,t,e){n.exports={default:e(94),__esModule:!0}},function(n,t,e){var a=e(11),i=a.JSON||(a.JSON={stringify:JSON.stringify});n.exports=function(n){return i.stringify.apply(i,arguments)}},function(n,t,e){"use strict";t.__esModule=!0;var a=r(e(96)),i=r(e(121));function r(n){return n&&n.__esModule?n:{default:n}}t.default=function(n,t){if(Array.isArray(n))return n;if((0,a.default)(Object(n)))return function(n,t){var e=[],a=!0,r=!1,o=void 0;try{for(var l,s=(0,i.default)(n);!(a=(l=s.next()).done)&&(e.push(l.value),!t||e.length!==t);a=!0);}catch(n){r=!0,o=n}finally{try{!a&&s.return&&s.return()}finally{if(r)throw o}}return e}(n,t);throw new TypeError("Invalid attempt to destructure non-iterable instance")}},function(n,t,e){n.exports={default:e(97),__esModule:!0}},function(n,t,e){e(48),e(59),n.exports=e(120)},function(n,t,e){"use strict";var a=e(99),i=e(100),r=e(14),o=e(25);n.exports=e(50)(Array,"Array",(function(n,t){this._t=o(n),this._i=0,this._k=t}),(function(){var n=this._t,t=this._k,e=this._i++;return!n||e>=n.length?(this._t=void 0,i(1)):i(0,"keys"==t?e:"values"==t?n[e]:[e,n[e]])}),"values"),r.Arguments=r.Array,a("keys"),a("values"),a("entries")},function(n,t){n.exports=function(){}},function(n,t){n.exports=function(n,t){return{value:t,done:!!n}}},function(n,t,e){var a=e(49);n.exports=Object("z").propertyIsEnumerable(0)?Object:function(n){return"String"==a(n)?n.split(""):Object(n)}},function(n,t,e){var a=e(12),i=e(11),r=e(103),o=e(15),l=e(19),s=function(n,t,e){var c,d,p,u=n&s.F,b=n&s.G,f=n&s.S,m=n&s.P,g=n&s.B,h=n&s.W,x=b?i:i[t]||(i[t]={}),w=x.prototype,k=b?a:f?a[t]:(a[t]||{}).prototype;for(c in b&&(e=t),e)(d=!u&&k&&void 0!==k[c])&&l(x,c)||(p=d?k[c]:e[c],x[c]=b&&"function"!=typeof k[c]?e[c]:g&&d?r(p,a):h&&k[c]==p?function(n){var t=function(t,e,a){if(this instanceof n){switch(arguments.length){case 0:return new n;case 1:return new n(t);case 2:return new n(t,e)}return new n(t,e,a)}return n.apply(this,arguments)};return t.prototype=n.prototype,t}(p):m&&"function"==typeof p?r(Function.call,p):p,m&&((x.virtual||(x.virtual={}))[c]=p,n&s.R&&w&&!w[c]&&o(w,c,p)))};s.F=1,s.G=2,s.S=4,s.P=8,s.B=16,s.W=32,s.U=64,s.R=128,n.exports=s},function(n,t,e){var a=e(104);n.exports=function(n,t,e){if(a(n),void 0===t)return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,a){return n.call(t,e,a)};case 3:return function(e,a,i){return n.call(t,e,a,i)}}return function(){return n.apply(t,arguments)}}},function(n,t){n.exports=function(n){if("function"!=typeof n)throw TypeError(n+" is not a function!");return n}},function(n,t,e){n.exports=!e(18)&&!e(52)((function(){return 7!=Object.defineProperty(e(53)("div"),"a",{get:function(){return 7}}).a}))},function(n,t,e){var a=e(28);n.exports=function(n,t){if(!a(n))return n;var e,i;if(t&&"function"==typeof(e=n.toString)&&!a(i=e.call(n)))return i;if("function"==typeof(e=n.valueOf)&&!a(i=e.call(n)))return i;if(!t&&"function"==typeof(e=n.toString)&&!a(i=e.call(n)))return i;throw TypeError("Can't convert object to primitive value")}},function(n,t,e){n.exports=e(15)},function(n,t,e){"use strict";var a=e(109),i=e(54),r=e(58),o={};e(15)(o,e(9)("iterator"),(function(){return this})),n.exports=function(n,t,e){n.prototype=a(o,{next:i(1,e)}),r(n,t+" Iterator")}},function(n,t,e){var a=e(17),i=e(110),r=e(57),o=e(30)("IE_PROTO"),l=function(){},s=function(){var n,t=e(53)("iframe"),a=r.length;for(t.style.display="none",e(116).appendChild(t),t.src="javascript:",(n=t.contentWindow.document).open(),n.write("