整理远程模块
Some checks are pending
TDevOPsCICD / build-image (push) Waiting to run

This commit is contained in:
cheney 2026-06-16 16:40:34 +08:00
parent b46cddc7cf
commit e05ce19eac
14 changed files with 1154 additions and 365 deletions

47
kit/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,47 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "remote_test",
"runtimeExecutable": "bun",
"runtimeArgs": ["test", "./test/remote_test.js", "-t", "exposes documented public methods", "--inspect-brk"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/**/*.js"]
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Debug File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "launch",
"name": "Run File",
"program": "${file}",
"cwd": "${workspaceFolder}",
"noDebug": true,
"watchMode": false
},
{
"type": "bun",
"internalConsoleOptions": "neverOpen",
"request": "attach",
"name": "Attach Bun",
"url": "ws://localhost:6499/e4xu5fgl1a",
"stopOnEntry": false
}
]
}

Binary file not shown.

View File

@ -0,0 +1,365 @@
- 基于 webdav 协议访问。包括**远程仓库存储方案**(基于 WebDAV 协议)与**本地客户端存储方案**。
- 支持存储多种操作系统Linux、Windows、多种架构X86、aarch64
- 支持记录官网地址,方便定期从官网拉取新版本存储。
---
## Part A远程仓库存储方案基于 WebDAV
### 1. 概述
本方案定义了一种基于 WebDAV 协议访问的远程仓库存储格式,用于存储开源项目的目标代码(二进制分发文件)。仓库支持:
- 多种操作系统Linux、Windows、macOS、FreeBSD 等)
- 多种硬件架构x86_64、aarch64、armv7、i386 等)
- 平台无关的二进制文件(如 Java JAR、Python 轮包、纯 JS 文件等)
- 单文件可执行程序与多文件压缩包zip、tar.gz 等)
- 版本管理(语义化版本)与元数据记录
仓库通过 WebDAV 的目录和文件操作进行访问,支持枚举、下载、上传、删除等操作。
### 2. 仓库根目录结构
```
/repo/ # WebDAV 服务暴露的根路径
└─ {project-id}/ # 项目目录(唯一标识)
├─ metadata.json # 项目级元数据文件
└─ {semver}/ # 版本目录(如 1.2.3 或 v1.2.3
├─ version.json # 版本级元数据文件(推荐)
├─ any/any/ # 平台无关文件目录(保留字)
│ └─ ... # 单文件或压缩包
├─ {os}/ # 操作系统(如 linux, windows, darwin
│ └─ {arch}/ # 架构(如 x86_64, aarch64
│ └─ ... # 具体二进制文件或压缩包
└─ ...
```
#### 2.1 命名规则
- `project-id`:小写字母、数字、连字符(-),长度 ≤ 64。
- `semver`:符合 [Semantic Versioning 2.0]( 的版本号,推荐不含 `v` 前缀。
- `os``arch` 使用标准化名称(见下表),`any` 为保留字,表示任意平台。
| 操作系统 | 标准名称 | 架构 | 标准名称 |
| ------- | --------- | ------- | --------- |
| Linux | `linux` | x86_64 | `x86_64` |
| Windows | `windows` | aarch64 | `aarch64` |
| macOS | `darwin` | armv7 | `armv7` |
| FreeBSD | `freebsd` | i386 | `i386` |
| 任意平台 | `any` | 任意架构 | `any` |
#### 2.2 目录说明
- **`any/any/`**:存放与操作系统和架构均无关的文件(如 JAR、纯 Python 脚本、WASM 模块)。客户端在任何平台均可直接使用此目录下的文件。
- **`{os}/{arch}/`**:存放特定平台、特定架构的二进制文件或压缩包。
- 同一版本下可同时包含 `any/any/` 和具体平台目录。
### 3. 元数据文件规范
#### 3.1 项目级元数据:`metadata.json`
位于每个项目根目录UTF-8 编码JSON 格式。
**字段说明**
| 字段名 | 类型 | 必选 | 说明 |
| ------------------ | ----------------- | --- | ----------------- |
| `project_id` | string | 是 | 项目唯一标识,与目录名相同 |
| `official_url` | string | 是 | 项目官网地址(用于定期拉取新版本) |
| `description` | string | 否 | 项目简述 |
| `latest_version` | string | 是 | 仓库中已存储的最新版本号 |
| `versions` | array of strings | 是 | 仓库中已存储的所有版本号列表 |
| `last_fetch_check` | string (ISO 8601) | 否 | 上次检查官网更新的时间戳 |
| `fetch_rules` | array of objects | 否 | 自动拉取时的匹配规则(见 3.3 |
| `extra` | object | 否 | 扩展字段 |
**示例**
```json
{
"project_id": "myapp",
"official_url": "",
"description": "An example tool",
"latest_version": "1.2.1",
"versions": ["1.0.0", "1.2.0", "1.2.1"],
"last_fetch_check": "2026-06-15T12:00:00Z",
"fetch_rules": [...],
"extra": { "license": "Apache-2.0" }
}
```
#### 3.2 版本级元数据:`version.json`
位于每个版本目录下,包含该版本的详细信息和文件清单。
**字段说明**
| 字段名 | 类型 | 必选 | 说明 |
| ----------------------- | ------------------ | ---- | ---------------------------------------------- |
| `version` | string | 是 | 版本号,与目录名一致 |
| `release_date` | string (ISO 8601) | 否 | 官方发布日期 |
| `official_download_url` | string | 否 | 官网的直接下载页或 API 地址 |
| `artifacts` | array of objects | 是 | 该版本包含的所有文件条目(见 3.2.1 |
| `checksums` | object | 否 | 文件路径到哈希值的映射(可选,与 artifacts 可共存) |
| `signatures` | object | 否 | 文件路径到数字签名的映射 |
##### 3.2.1 `artifacts` 数组中每个对象的字段
| 字段名 | 类型 | 必选 | 说明 |
| ----------------- | --------- | ---- | ------------------------------------------------------------ |
| `path` | string | 是 | 相对于版本目录的文件路径,如 `linux/x86_64/myapp``any/any/app.zip` |
| `type` | string | 是 | 文件类型:`executable`(单文件可执行)、`archive`(压缩包)、`library`(库文件)、`data`(资源数据) |
| `archive_format` | string | 否 | 当 `type="archive"` 时,取值为 `zip`、`tar.gz`、`tar.xz` 等 |
| `entry_point` | string | 否 | 当 `type="archive"` 时,指定压缩包内的可执行文件相对路径(如 `bin/app` |
| `unpack` | boolean | 否 | 是否建议客户端自动解压,默认 `false`。若为 `true`,客户端解压后应运行 `entry_point` |
| `checksum` | string | 否 | 该文件的哈希值(格式 `算法:值`,如 `sha256:abc...` |
**示例**
- **单文件可执行Linux**
```json
{
"version": "1.0.0",
"artifacts": [
{
"path": "linux/x86_64/myapp",
"type": "executable"
}
],
"checksums": {
"linux/x86_64/myapp": "sha256:e3b0c442..."
}
}
```
- **多文件压缩包Windows**
```json
{
"version": "2.0.0",
"artifacts": [
{
"path": "windows/x86_64/myapp.zip",
"type": "archive",
"archive_format": "zip",
"entry_point": "myapp/bin/myapp.exe",
"unpack": true
}
]
}
```
- **平台无关 JAR 文件**
```json
{
"version": "3.0.0",
"artifacts": [
{
"path": "any/any/myapp.jar",
"type": "executable"
}
]
}
```
#### 3.3 自动拉取规则(`fetch_rules`
`metadata.json` 中可定义 `fetch_rules` 数组,用于指导自动化工具从官网下载并分类存储。
每个规则对象包含:
| 字段名 | 类型 | 说明 |
| ----------------- | --------- | ------------------------------------------------------------ |
| `url_pattern` | string | 正则表达式,匹配下载 URL |
| `os` | string | 目标操作系统(`linux`、`windows`、`darwin`、`any` 等) |
| `arch` | string | 目标架构(`x86_64`、`aarch64`、`any` 等) |
| `type` | string | 文件类型(`executable` 或 `archive` |
| `archive_format` | string | 可选,当 `type="archive"` 时指定 |
| `entry_point` | string | 可选,压缩包内的入口点 |
| `unpack` | boolean | 可选,是否自动解压 |
**示例**
```json
"fetch_rules": [
{
"url_pattern": ".*-linux-amd64\\.tar\\.gz$",
"os": "linux",
"arch": "x86_64",
"type": "archive",
"archive_format": "tar.gz",
"entry_point": "myapp/bin/myapp",
"unpack": true
},
{
"url_pattern": ".*\\.jar$",
"os": "any",
"arch": "any",
"type": "executable"
}
]
```
### 4. 客户端访问方式
基于 WebDAV 协议,客户端可通过以下 HTTP 方法操作:
- `PROPFIND`列举项目、版本、os/arch 目录。
- `GET`:下载二进制文件或元数据文件。
- `PUT` / `MKCOL`:上传新版本或新文件(用于手动或自动填充)。
- `DELETE`:删除过期版本。
推荐 WebDAV 服务器配置:启用目录列表、支持 CORS、支持 HTTPS。
### 5. 自动化拉取新版本流程
外部工具(如 cron 作业)按以下步骤更新仓库:
1. 遍历仓库根目录下每个项目的 `metadata.json`
2. 访问 `official_url`,获取所有可用版本列表及最新版本。
3. 对比已存储版本,找出缺失的新版本。
4. 对于每个新版本,根据 `fetch_rules`(或内置逻辑)匹配下载 URL下载对应的二进制/压缩包。
5. 按 `os` / `arch` / `any` 规则存放到 `{project-id}/{version}/{os}/{arch}/` 下。
6. 计算文件哈希,生成或更新该版本的 `version.json`
7. 更新项目级 `metadata.json``versions`、`latest_version`、`last_fetch_check`)。
---
## Part B本地客户端存储方案用于版本管理与回退
### 1. 概述
客户端从远程 WebDAV 仓库下载项目的目标代码后,需要在本地持久化存储,并支持:
- 多版本共存
- 当前激活版本的快速切换(符号链接)
- 版本回退(无需重新下载)
- 压缩包自动解压与入口点管理
- 元数据记录(安装历史、校验信息)
- 可配置的旧版本清理策略
### 2. 本地根目录结构
客户端约定一个根目录(如 `~/.cache/artifact-repo/``~/.local/share/artifact-repo/`),其下按项目组织:
```
~/.local/share/artifact-repo/
└─ {project-id}/ # 项目目录(与远程 project-id 一致)
├─ .meta/ # 元数据目录(隐藏)
│ ├─ install.json # 本地安装记录
│ └─ history/ # 可选,操作历史 JSON 文件
├─ versions/ # 所有已下载的版本
│ ├─ {semver}/ # 版本目录(如 1.2.0
│ │ ├─ .version.json # 从远程复制的 version.json附加本地信息
│ │ └─ {os}/{arch}/ # 与远程结构一致的实际文件
│ │ └─ ...
│ └─ {semver}/...
└─ current -> versions/{semver} # 符号链接,指向当前激活的版本
```
> **注意**:符号链接可放在项目根目录下,也可放在 `.meta/current`。推荐项目根目录下的 `current`,便于外部脚本直接引用(如 `$REPO_HOME/myapp/current/bin/myapp`)。
### 3. 元数据文件 `install.json`
位于 `.meta/install.json`,记录项目的本地安装状态。
**字段说明**
| 字段名 | 类型 | 说明 |
| -------------------- | ------------------ | ------------------------------------------------------------ |
| `project_id` | string | 项目标识 |
| `installed_versions` | array of strings | 已下载到本地的所有版本号 |
| `current_version` | string | 当前激活的版本号(应与 `current` 符号链接一致) |
| `last_updated` | string (ISO 8601) | 最后一次变更时间(安装、切换、删除) |
| `history` | array of objects | 操作历史记录,每个元素包含 `timestamp`, `action`, `version`, `from`(可选) |
| `options` | object | 客户端配置,如 `auto_cleanup`, `max_versions` 等 |
**示例**
```json
{
"project_id": "myapp",
"installed_versions": ["1.0.0", "1.2.0", "1.2.1"],
"current_version": "1.2.1",
"last_updated": "2026-06-15T14:30:00Z",
"history": [
{
"timestamp": "2026-06-10T09:00:00Z",
"action": "install",
"version": "1.0.0"
},
{
"timestamp": "2026-06-15T14:30:00Z",
"action": "switch",
"version": "1.2.1",
"from": "1.2.0"
}
],
"options": {
"auto_cleanup": true,
"max_versions": 5
}
}
```
### 4. 版本目录中的 `.version.json`
从远程仓库的 `version.json` 复制而来,并增加本地特有字段:
```json
{
"version": "1.2.1",
"installed_at": "2026-06-15T14:25:00Z",
"local_checksums": {
"linux/x86_64/myapp": "sha256:..."
},
... // 原始远程字段artifacts, release_date 等)
}
```
### 5. 核心操作流程
#### 5.1 安装新版本(更新)
1. 从远程 WebDAV 仓库下载指定版本的所有相关文件到 `versions/{new_version}/`,保持远程目录结构(包括 `any/any/` 和具体平台目录)。
2. 同时下载 `version.json` 并保存为 `.version.json`,添加 `installed_at` 时间戳。
3. 更新 `.meta/install.json`
- 将 `new_version` 加入 `installed_versions`(若未存在)。
- 设置 `current_version``new_version`(若需要立即激活)。
- 添加历史记录(`action: "install"` 或 `"switch"`)。
4. 更新项目根目录下的 `current` 符号链接指向 `versions/{new_version}`
5. 如果 `artifacts` 中有 `type="archive"``unpack=true` 的条目,则解压到版本目录下的特定子目录(如 `extracted/`),并在 `.version.json` 中记录解压后的入口点路径。
6. 根据 `options` 中的清理策略删除旧版本(保留最近 N 个版本或保留指定天数)。
#### 5.2 回退到已安装的旧版本
1. 确认目标旧版本存在于 `installed_versions` 中。
2. 修改 `current` 符号链接指向 `versions/{old_version}`
3. 更新 `.meta/install.json`
- 将 `current_version` 改为 `old_version`
- 添加历史记录(`action: "rollback"` 或 `"switch"`,可记录 `from` 原版本)。
4. 无需重新下载任何文件。
#### 5.3 卸载特定版本
1. 如果卸载的是当前激活版本,先切换到另一个已安装版本(或提示用户)。
2. 删除 `versions/{version}` 整个目录。
3. 从 `install.json``installed_versions` 数组中移除该版本。
4. 添加历史记录(`action: "uninstall"`)。
#### 5.4 清理旧版本(自动或手动)
清理策略示例:
- 保留最近 `max_versions` 个版本(不计当前版本)。
- 或删除 `keep_recent_days` 天之前安装且不是当前版本的版本。
- 每次安装新版本后自动触发清理。
### 6. 处理压缩包archive的特别说明
当远程 `artifact``unpack=true` 时:
- 客户端下载压缩包到 `versions/{version}/{path}`
- 立即解压到同目录下的 `_extracted/` 子目录(例如 `versions/1.2.1/linux/x86_64/_extracted/`)。
- 根据 `entry_point``.version.json` 中记录解压后的可执行文件绝对路径(或相对于版本目录的路径)。
- 符号链接 `current` 可以直接指向解压后的入口点文件,而不是整个版本目录。实现方式:
```
current/bin/myapp -> ../versions/1.2.1/_extracted/bin/myapp
```
这样切换版本时只需更新符号链接,无需重新解压。
### 7. 多平台兼容性
客户端在下载时,应根据当前运行的操作系统和架构,仅从远程仓库获取匹配的文件:
- 优先尝试 `{os}/{arch}/`
- 若不存在,回退到 `any/any/`
- 下载后保持远程的相对路径结构,但客户端实际运行时只使用与当前平台相关的文件。
在本地存储中,所有版本目录仍保留完整的 `{os}/{arch}/``any/any/` 结构,便于在不同平台间迁移或共享缓存。

View File

@ -0,0 +1,27 @@
```bash
# 验证远程仓库可达
check
# 发行某个模块 eg. myapp
publish myapp
# 列出已安装版本 eg. myapp
list myapp
# 输出:
# 1.0.0
# * 1.2.0 (current)
# 1.2.1
# 安装某个模块 eg. myapp
install myapp
# 切换到版本 1.2.0
use myapp@1.2.0
# 回滚到上一个版本
rollback myapp
# 删除模块
remove myapp
```

View File

@ -30,7 +30,11 @@ module.exports = class SystemConfig {
static getLocalKitPath(){
let path = "./"
if ( this.isWindows() ) {
path = require('path').dirname(require.main.filename)
if( require && require.main && require.main.filename ){
path = require('path').dirname(require.main.filename)
} else {
path = process.cwd()
}
} else {
path = require('path').dirname(process.execPath);
}

View File

@ -35,16 +35,16 @@ module.exports = {
let file = cli.getParamValue("file");
if (!file) {
file = "kit"
file = "kitx"
}
const scriptFile = `./${file}.js`;
const stat = fs.existsSync(scriptFile)
if (!stat) {
if (!cli.getParamValue("file")) {
fs.writeFileSync("./kit.js", `
fs.writeFileSync("./kitx.js", `
async function main(){
console.log("hello kit!")
console.log("hello kit!");
}
`);
} else {
@ -82,7 +82,7 @@ async function main(){
}
});
parser.addCmdLine("remote", "验证远程仓库;", async function () {
parser.addCmdLine("remote", "验证远程仓库可达;", async function () {
let remote = await $context.get("remote")
if (!(await remote.check())) {
$logger.error("远程仓库不可用")

View File

@ -329,7 +329,11 @@ function stringifyLogValue(value) {
function printCallLog(name, args, result) {
console.log(`调用 ${name} 功能`);
console.log(`入参 ${args.map(stringifyLogValue).join(" ")}`);
console.log(`结果 ${stringifyLogValue(result)}`);
if (result === undefined) {
console.log("执行结束");
} else {
console.log(`执行结果 ${stringifyLogValue(result)}`);
}
}
function withCallLog(name, fn) {

View File

@ -1,239 +1,366 @@
/**
* remote 模块用来处理远程仓库相关事务
*/
const path = require("path")
const fs = require("fs")
const JSON5 = require('json5')
const { pipeline, Transform} = require("stream/promises");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { pipeline } = require("stream/promises");
const JSON5 = require("json5");
function now() {
return new Date().toISOString();
}
function normalizeRemotePath(value) {
if (!value || value === "/") {
return "/";
}
return path.posix.normalize("/" + String(value).replace(/\\/g, "/")).replace(/\/+$|^$/g, "") || "/";
}
function normalizeOs(value) {
const name = String(value || os.platform()).toLowerCase();
if (["windows_nt", "win32", "windows"].includes(name)) {
return "windows";
}
if (["macos", "mac", "darwin"].includes(name)) {
return "darwin";
}
if (["linux", "alpine"].includes(name)) {
return "linux";
}
return name;
}
function normalizeArch(value) {
const name = String(value || os.arch()).toLowerCase();
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
return "x86_64";
}
if (["arm64", "aarch64"].includes(name)) {
return "aarch64";
}
if (["ia32", "i386", "x86"].includes(name)) {
return "i386";
}
return name;
}
function localModuleRoot(projectId) {
return path.resolve($sConfig.getLocalStorePath(), "modules", projectId);
}
function readJsonFile(filePath, fallback) {
try {
if (fs.existsSync(filePath)) {
return JSON5.parse(fs.readFileSync(filePath, "utf-8"));
}
} catch (e) {
$logger.warn(`读取 JSON 失败 ${filePath}: ${e.message}`);
}
return fallback;
}
function writeJsonFile(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 4), "utf-8");
}
function safeRemove(target) {
fs.rmSync(target, { recursive: true, force: true });
}
function copyRecursive(from, to) {
const stat = fs.statSync(from);
if (stat.isDirectory()) {
fs.mkdirSync(to, { recursive: true });
for (const item of fs.readdirSync(from)) {
copyRecursive(path.join(from, item), path.join(to, item));
}
} else {
fs.mkdirSync(path.dirname(to), { recursive: true });
fs.copyFileSync(from, to);
}
}
module.exports = class Remote {
constructor() {
// this.__init()
this.func_publish = require("./publish")
this.func_install = require("./install")
this.func_publish = require("./publish");
this.func_install = require("./install");
this.root = "/";
}
async init() {
this.webdav = await import("webdav");
const webdav = await import("webdav");
this.webdav = webdav.createClient ? webdav : webdav.default;
const uConfig = await $context.get("uConfig");
if ( uConfig.get("remoteServer") ){
let server = uConfig.get("remoteServer");
$logger.info("server " + server)
if ( server.startsWith("webdav") ) {
server = "http" + server.substring(6)
}
if ( uConfig.get("remoteAuth") ) {
let up = uConfig.get("remoteAuth");
$logger.info("remoteAuth " + up)
up = up.split(":", 2)
this.client = this.webdav.createClient(
server,
{
username: up[0],
password: up[1]
}
);
} else {
this.client = this.webdav.createClient(
server
);
}
let server = uConfig.get("remoteServer");
if (!server) {
return;
}
if (server.startsWith("webdav")) {
server = "http" + server.substring(6);
}
const options = {};
const auth = uConfig.get("remoteAuth");
if (auth) {
const [username, password] = auth.split(":", 2);
options.username = username;
options.password = password;
}
this.client = this.webdav.createClient(server, options);
}
async check() {
if ( ! this.webdav ) {
$logger.info("webdav unable")
return false
}
if ( ! this.client ) {
$logger.info("client unable")
return false
if (!this.client) {
$logger.info("remote client unavailable");
return false;
}
try {
// Get directory contents
const directoryItems = await this.client.getDirectoryContents("/");
$logger.info("directoryItems", directoryItems)
// 是否包含 index.json ?
// 不包含尝试创建
// 包含了尝试下载
const indexJsonFile = this.getIndexJson( directoryItems );
$logger.info("indexJsonFile", indexJsonFile)
if ( indexJsonFile ) {
// 包含了尝试下载
// 下载有缓存
this.download(indexJsonFile)
$logger.info("download index")
} else {
// 不包含尝试创建
await this.affireRemoteStore()
$logger.info("affireRemoteStore")
}
return true
}catch ( e ) {
$logger.error("remote check 过程中出错", e)
console.error(e)
return false
}
}
/**
* 先对比时间和长度有不同才会真正下载否则使用本地缓存
* 本地缓存路径即本地仓库路径
* @param rFile
*/
download(rFile){
const lFilePath = path.resolve($sConfig.getLocalStorePath(), rFile.filename);
if ( fs.existsSync( lFilePath ) ) {
const lFileInfo = fs.statSync(lFilePath);
if ( lFileInfo.size === rFile.length
&& lFileInfo.mtime === rFile.lastmod
) {
// 使用缓存
$logger.debug("use cache")
return true
}
}
// 下载并覆盖
// 不知道是同步还是异步
this.wDownload( rFile.filename, lFilePath );
}
async affireRemoteStore(){
// Get directory contents
const directoryItems = await this.client.getDirectoryContents("/");
// 是否包含 index.json ?
// 不包含尝试创建
// 包含了尝试下载
const indexJsonFile = this.getIndexJson( directoryItems );
if ( ! indexJsonFile ) {
// 创建 index
await this.client.putFileContents("/index.json", JSON5.stringify({
modules : [],
tmpls : [],
lists : []
}, null, 4) , { overwrite: false });
}
const modulesFolder = this.getFileInfoByName(directoryItems, "modules")
if ( ! modulesFolder ) {
// 创建文件夹
await this.client.createDirectory("/modules");
}
const tmplsFolder = this.getFileInfoByName(directoryItems, "tmpls")
if ( ! tmplsFolder ) {
// 创建文件夹
await this.client.createDirectory("/tmpls");
}
const listsFolder = this.getFileInfoByName(directoryItems, "lists")
if ( ! listsFolder ) {
// 创建文件夹
await this.client.createDirectory("/lists");
}
}
getIndexJson(items){
return this.getFileInfoByName( items, "index.json" );
}
getFileInfoByName(items, name){
if ( ! items || items.length < 1 ) {
await this.ensureDir("/");
await this.client.getDirectoryContents("/");
return true;
} catch (e) {
$logger.error("remote check 过程中出错 " + e.message);
return false;
}
for ( let item of items ) {
if ( item.basename === name ) {
return item;
}
async publish(localFile, remotePath) {
return await this.func_publish(this, localFile, remotePath);
}
async install(id) {
return await this.func_install(this, id);
}
async list(projectId) {
const state = this.getInstallState(projectId);
for (const version of state.installed_versions) {
const current = version === state.current_version ? "*" : " ";
$logger.info(`${current} ${version}${current === "*" ? " (current)" : ""}`);
}
return state.installed_versions;
}
async use(projectVersion) {
const { projectId, version } = this.parseProjectVersion(projectVersion);
return this.activate(projectId, version, "switch");
}
async rollback(projectId) {
const state = this.getInstallState(projectId);
const history = [...state.history].reverse();
const current = state.current_version;
const previous = history.find((item) => item.version && item.version !== current && state.installed_versions.includes(item.version));
if (!previous) {
throw new Error(`没有可回滚版本 ${projectId}`);
}
return this.activate(projectId, previous.version, "rollback");
}
async remove(projectVersion) {
const { projectId, version } = this.parseProjectVersion(projectVersion);
const root = localModuleRoot(projectId);
const state = this.getInstallState(projectId);
const targetVersion = version || state.current_version;
if (!targetVersion) {
return false;
}
if (state.current_version === targetVersion) {
throw new Error(`不能删除当前激活版本 ${projectId}@${targetVersion}`);
}
safeRemove(path.resolve(root, "versions", targetVersion));
state.installed_versions = state.installed_versions.filter((item) => item !== targetVersion);
this.pushHistory(state, "uninstall", targetVersion);
this.saveInstallState(projectId, state);
return true;
}
parseProjectVersion(value) {
const text = String(value || "kit");
const index = text.lastIndexOf("@");
if (index <= 0) {
return { projectId: text, version: undefined };
}
return { projectId: text.substring(0, index), version: text.substring(index + 1) };
}
getLocalModuleRoot(projectId) {
const root = localModuleRoot(projectId);
fs.mkdirSync(root, { recursive: true });
return root;
}
getInstallState(projectId) {
const statePath = path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json");
const state = readJsonFile(statePath, {
project_id: projectId,
installed_versions: [],
current_version: undefined,
last_updated: undefined,
history: [],
options: {}
});
state.project_id = state.project_id || projectId;
state.installed_versions = state.installed_versions || [];
state.history = state.history || [];
state.options = state.options || {};
return state;
}
saveInstallState(projectId, state) {
state.last_updated = now();
writeJsonFile(path.resolve(this.getLocalModuleRoot(projectId), ".meta", "install.json"), state);
}
pushHistory(state, action, version, from) {
state.history.push({ timestamp: now(), action, version, from });
}
async activate(projectId, version, action = "switch") {
const root = this.getLocalModuleRoot(projectId);
const versionDir = path.resolve(root, "versions", version);
if (!fs.existsSync(versionDir)) {
throw new Error(`本地版本不存在 ${projectId}@${version}`);
}
const current = path.resolve(root, "current");
safeRemove(current);
try {
fs.symlinkSync(path.relative(root, versionDir), current, "junction");
} catch (e) {
copyRecursive(versionDir, current);
}
const state = this.getInstallState(projectId);
const from = state.current_version;
state.current_version = version;
if (!state.installed_versions.includes(version)) {
state.installed_versions.push(version);
}
this.pushHistory(state, action, version, from);
this.saveInstallState(projectId, state);
fs.writeFileSync(path.resolve(root, "version"), version, "utf-8");
return version;
}
getPlatformCandidates() {
return [
{ os: normalizeOs(os.platform()), arch: normalizeArch(os.arch()) },
{ os: "any", arch: "any" }
];
}
async ensureDir(remotePath) {
const target = normalizeRemotePath(remotePath);
if (target === "/") {
return;
}
if (await this.client.exists(target)) {
return;
}
await this.ensureDir(path.posix.dirname(target));
await this.client.createDirectory(target);
}
async putJson(remotePath, data) {
await this.ensureDir(path.posix.dirname(remotePath));
await this.client.putFileContents(normalizeRemotePath(remotePath), JSON.stringify(data, null, 4), { overwrite: true });
}
async getJson(remotePath, fallback) {
try {
const text = await this.client.getFileContents(normalizeRemotePath(remotePath), { format: "text" });
return JSON5.parse(text);
} catch (e) {
return fallback;
}
}
async wUpload(from, to) {
await this.ensureDir(path.posix.dirname(to));
const rs = fs.createReadStream(from);
const ws = this.client.createWriteStream(normalizeRemotePath(to));
await pipeline(rs, ws);
}
async wDownload(from, to) {
fs.mkdirSync(path.dirname(to), { recursive: true });
const rs = this.client.createReadStream(normalizeRemotePath(from));
const ws = fs.createWriteStream(to);
await pipeline(rs, ws);
}
async wMkdir(folder) {
return this.ensureDir(folder);
}
async wGetModuleInfo(projectId) {
return this.getProjectMetadata(projectId);
}
async getProjectMetadata(projectId) {
const metadata = await this.getJson(`/${projectId}/metadata.json`, undefined);
if (metadata) {
return metadata;
}
const legacy = await this.getJson(`/modules/${projectId}/meta.json`, undefined);
if (legacy) {
return {
project_id: legacy.id || projectId,
description: legacy.desc || legacy.name || "",
latest_version: legacy.latest && legacy.latest.version,
versions: (legacy.versions || []).map((item) => item.version || item),
extra: legacy
};
}
throw new Error(`远程模块不存在 ${projectId}`);
}
async getVersionMetadata(projectId, version) {
const metadata = await this.getJson(`/${projectId}/${version}/version.json`, undefined);
if (metadata) {
return metadata;
}
const project = await this.getProjectMetadata(projectId);
const legacyVersion = project.extra && project.extra.versions && project.extra.versions.find((item) => (item.version || item) === version);
if (legacyVersion && legacyVersion.dist) {
return {
version,
artifacts: legacyVersion.dist.map((dist) => ({
path: `${normalizeOs(dist.os)}/${normalizeArch(dist.platform)}/${dist.filename}`,
type: "executable"
}))
};
}
throw new Error(`远程版本不存在 ${projectId}@${version}`);
}
async downloadArtifact(projectId, version, artifact, versionDir) {
const remotePath = `/${projectId}/${version}/${artifact.path}`;
const localPath = path.resolve(versionDir, artifact.path);
await this.wDownload(remotePath, localPath);
return localPath;
}
getMatchingArtifacts(versionMetadata) {
const artifacts = versionMetadata.artifacts || [];
const candidates = this.getPlatformCandidates();
for (const candidate of candidates) {
const prefix = `${candidate.os}/${candidate.arch}/`;
const matched = artifacts.filter((item) => String(item.path).replace(/\\/g, "/").startsWith(prefix));
if (matched.length > 0) {
return matched;
}
}
return false;
return [];
}
async publish(localFile, remotePath){
return await this.func_publish( this , localFile, remotePath )
}
async install(id){
return this.func_install( this, id )
}
async wUpload(from, to, debug){
$logger.info(`from=${from} to=${to}`)
const totalSize = fs.statSync(from).size;
debug ? console.log("totalSize", totalSize) : null
const rs = fs.createReadStream( from );
let wSize = 0;
let rSize = 0;
rs.on("data", (r)=>{
rSize += r.length;
debug ? console.log("r", rSize * 100 / totalSize + "%") : null
})
const ws = this.client.createWriteStream(to)
ws.on("data", (r)=>{
wSize += r.length;
debug ? console.log("w ", wSize * 100 / totalSize + "%") : null
})
await pipeline( rs, ws);
debug ? console.log("finish") : null
}
async wDownload(from, to){
$logger.info("download", from, to )
const stat = await this.client.stat(from);
const totalSize = stat.size;
let wSize = 0;
let rSize = 0;
const rs = this.client.createReadStream(from);
rs.on("data", (r)=>{
rSize += r.length;
// console.log("r", rSize * 100 / totalSize)
})
const ws = fs.createWriteStream(to);
ws.on("data", (r)=>{
wSize += r.length;
// console.log("w", wSize * 100 / totalSize)
})
return await pipeline(
rs, ws
)
}
async wMkdir(folder){
const folderfather = path.dirname( folder );
if ( ! await this.client.exists( folderfather ) ) {
// await remote.client.createDirectory( folderfather )
await this.wMkdir( folderfather )
}
if ( await this.client.exists( folder ) ) {
return
} else {
await this.client.createDirectory( folder )
}
}
async wGetModuleInfo(id){
let meta = await this.client.getFileContents("/modules/" + id + "/meta.json", { format: "text" });
let metaJson = JSON5.parse( meta );
return metaJson
}
}

View File

@ -1,53 +1,83 @@
const os = require('os');
const fs = require("fs");
const osutil = require("../util/osutil")
const fs = require("fs");
const path = require("path");
const { execFileSync } = require("child_process");
function matchDistName(cur, dists){
const list = []
for (let dist of dists) {
if ( cur.curOS === dist.os && cur.curPlatform === dist.platform) {
list.push( dist )
}
else if ( cur.curOS === dist.os && "cross" === dist.platform) {
list.unshift( dist )
}
else if ( "cross" === dist.os && cur.curPlatform === dist.platform) {
list.unshift( dist )
}
}
return list
function now() {
return new Date().toISOString();
}
function verifyChecksum(filePath, checksum) {
if (!checksum) {
return;
}
const [alg, expected] = checksum.split(":", 2);
if (!alg || !expected) {
return;
}
const crypto = require("crypto");
const actual = crypto.createHash(alg).update(fs.readFileSync(filePath)).digest("hex");
if (actual !== expected) {
throw new Error(`文件校验失败 ${filePath}`);
}
}
module.exports = async function install(remote, id){
const cur = osutil.cur();
if ( ! id ) {
id = "kit"
function unpackArtifact(artifact, localPath, versionDir) {
if (artifact.type !== "archive" || !artifact.unpack) {
return;
}
// 获取本地版本
const localInfo = osutil.choise([
require("../windows/local").getModuleInfo,
require("../linux/local").getModuleInfo,
])( id )
const extractDir = path.resolve(path.dirname(localPath), "_extracted");
fs.mkdirSync(extractDir, { recursive: true });
const format = artifact.archive_format || "zip";
if (format === "zip") {
if (process.platform === "win32") {
const source = localPath.replace(/'/g, "''");
const target = extractDir.replace(/'/g, "''");
execFileSync("powershell.exe", ["-NoProfile", "-Command", `Expand-Archive -Path '${source}' -DestinationPath '${target}' -Force`], { stdio: "inherit" });
} else {
execFileSync("unzip", ["-o", localPath, "-d", extractDir], { stdio: "inherit" });
}
} else if (["tar", "tar.gz", "tgz", "tar.xz"].includes(format)) {
execFileSync("tar", ["-xf", localPath, "-C", extractDir], { stdio: "inherit" });
}
$logger.info("local version", localInfo.version)
artifact.extracted_path = path.relative(versionDir, extractDir).replace(/\\/g, "/");
if (artifact.entry_point) {
artifact.extracted_entry_point = path.posix.join(artifact.extracted_path, artifact.entry_point.replace(/\\/g, "/"));
}
}
// 确认远程版本文件是否存在
const remoteInfo = await remote.wGetModuleInfo(id)
$logger.info("remote latest version", remoteInfo.latest.version)
module.exports = async function install(remote, id) {
const parsed = remote.parseProjectVersion(id || "kit");
const projectId = parsed.projectId;
const project = await remote.getProjectMetadata(projectId);
const version = parsed.version || project.latest_version;
if (!version) {
throw new Error(`远程模块没有可安装版本 ${projectId}`);
}
const dists = matchDistName(cur, remoteInfo.latest.dist)
const dist = dists.pop()
const versionMeta = await remote.getVersionMetadata(projectId, version);
const artifacts = remote.getMatchingArtifacts(versionMeta);
if (artifacts.length < 1) {
throw new Error(`没有匹配当前平台的文件 ${projectId}@${version}`);
}
await remote.wDownload(
"/modules/" + id + "/" + remoteInfo.latest.version + "/" + cur.curOS + "/" + cur.curPlatform + "/" + dist.filename,
localInfo.homeDir + "/" + dist.filename,
)
const root = remote.getLocalModuleRoot(projectId);
const versionDir = path.resolve(root, "versions", version);
fs.mkdirSync(versionDir, { recursive: true });
fs.writeFileSync( localInfo.homeDir + "/version", remoteInfo.latest.version );
for (const artifact of artifacts) {
const localPath = await remote.downloadArtifact(projectId, version, artifact, versionDir);
verifyChecksum(localPath, artifact.checksum || (versionMeta.checksums && versionMeta.checksums[artifact.path]));
unpackArtifact(artifact, localPath, versionDir);
}
$logger.info("当前版本", remoteInfo.latest.version )
const localVersionMeta = Object.assign({}, versionMeta, { installed_at: now(), artifacts });
fs.writeFileSync(path.resolve(versionDir, ".version.json"), JSON.stringify(localVersionMeta, null, 4), "utf-8");
}
const state = remote.getInstallState(projectId);
const action = state.current_version ? "switch" : "install";
await remote.activate(projectId, version, action);
$logger.info(`当前版本 ${projectId}@${version}`);
return version;
};

View File

@ -1,107 +1,176 @@
const path = require("path")
const fs = require("fs")
const JSON5 = require('json5')
const { pipeline } = require('stream/promises');
const objutil = require("../util/objutil")
const datautil = require("../util/datautil")
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const JSON5 = require("json5");
function now() {
return new Date().toISOString();
}
function normalizeOs(value) {
const name = String(value || "any").toLowerCase();
if (["windows_nt", "win32", "windows"].includes(name)) {
return "windows";
}
if (["macos", "mac", "darwin"].includes(name)) {
return "darwin";
}
if (["linux", "alpine"].includes(name)) {
return "linux";
}
if (["cross", "all", "any"].includes(name)) {
return "any";
}
return name;
}
function normalizeArch(value) {
const name = String(value || "any").toLowerCase();
if (["x64", "x86-64", "x86_64", "amd64"].includes(name)) {
return "x86_64";
}
if (["arm64", "aarch64"].includes(name)) {
return "aarch64";
}
if (["ia32", "i386", "x86"].includes(name)) {
return "i386";
}
if (["cross", "all", "any"].includes(name)) {
return "any";
}
return name;
}
function sha256(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function guessArtifactType(filePath) {
const ext = path.extname(filePath).toLowerCase();
if ([".zip", ".tgz", ".gz", ".xz", ".tar"].includes(ext)) {
return "archive";
}
return "executable";
}
function archiveFormat(filePath) {
const name = path.basename(filePath).toLowerCase();
if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) {
return "tar.gz";
}
if (name.endsWith(".tar.xz")) {
return "tar.xz";
}
if (name.endsWith(".zip")) {
return "zip";
}
if (name.endsWith(".tar")) {
return "tar";
}
return undefined;
}
function buildVersion(localMeta) {
if (localMeta.release_version) {
return localMeta.release_version;
}
if (localMeta.version) {
return localMeta.version;
}
return new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
}
function readLocalMeta(localFile) {
const base = localFile ? path.resolve(localFile) : process.cwd();
const metaFile = fs.statSync(base).isDirectory() ? path.resolve(base, "meta.json") : path.resolve(process.cwd(), "meta.json");
if (fs.existsSync(metaFile)) {
return JSON5.parse(fs.readFileSync(metaFile, "utf-8"));
}
if (!localFile) {
throw new Error("当前路径不包含 meta.json");
}
return {
id: path.basename(localFile, path.extname(localFile)).toLowerCase(),
version: buildVersion({}),
desc: "",
dist: [{ path: localFile, os: "any", platform: "any", filename: path.basename(localFile) }]
};
}
function buildArtifacts(localMeta) {
const dists = localMeta.dist || localMeta.artifacts || [];
return dists.map((dist) => {
const localPath = path.resolve(dist.path || dist.localPath || dist.file || dist.filename);
const osName = normalizeOs(dist.os);
const archName = normalizeArch(dist.arch || dist.platform);
const filename = dist.filename || path.basename(localPath);
const artifactPath = dist.remotePath || `${osName}/${archName}/${filename}`;
const type = dist.type || guessArtifactType(localPath);
const artifact = {
path: artifactPath.replace(/\\/g, "/"),
type,
checksum: `sha256:${sha256(localPath)}`,
localPath
};
if (type === "archive") {
artifact.archive_format = dist.archive_format || archiveFormat(localPath);
artifact.entry_point = dist.entry_point;
artifact.unpack = Boolean(dist.unpack);
}
return artifact;
});
}
module.exports = async function publish(remote, localFile, remotePath) {
if (remotePath) {
const target = path.posix.resolve("/", remotePath.replace(/\\/g, "/"));
await remote.wUpload(localFile, path.posix.join(target, path.basename(localFile)));
$logger.info("文件上传成功");
return;
}
if( remotePath ) {
const localMeta = readLocalMeta(localFile);
const projectId = localMeta.project_id || localMeta.id || localMeta.name;
if (!projectId) {
throw new Error("meta.json 缺少 project_id/id/name");
}
// 如果有 meta
const version = buildVersion(localMeta);
const artifacts = buildArtifacts(localMeta);
await remote.ensureDir(`/${projectId}/${version}`);
// 如果没有 meta
$logger.info("远程路径没有 meta, 按照默认规则上传");
const versionMeta = {
version,
release_date: localMeta.release_date || now(),
official_download_url: localMeta.official_download_url || "",
artifacts: artifacts.map(({ localPath, ...artifact }) => artifact),
checksums: Object.fromEntries(artifacts.map((artifact) => [artifact.path, artifact.checksum]))
};
// 确认目录
let folder = path.posix.resolve("/" + remotePath + "/data/" + datautil.now() );
if ( ! await remote.client.exists( folder ) ) {
// 创建文件夹
await remote.client.createDirectory(folder, {
recursive: true
});
}
for (const artifact of artifacts) {
await remote.wUpload(artifact.localPath, `/${projectId}/${version}/${artifact.path}`);
}
await remote.putJson(`/${projectId}/${version}/version.json`, versionMeta);
let filename = path.basename(localFile);
const metadata = await remote.getJson(`/${projectId}/metadata.json`, {
project_id: projectId,
official_url: localMeta.official_url || localMeta.vcs || "",
description: localMeta.description || localMeta.desc || "",
latest_version: version,
versions: [],
fetch_rules: localMeta.fetch_rules || [],
extra: {}
});
// 上传文件
await remote.wUpload( localFile , path.posix.resolve(folder , filename ));
metadata.project_id = projectId;
metadata.official_url = metadata.official_url || localMeta.official_url || localMeta.vcs || "";
metadata.description = localMeta.description || localMeta.desc || metadata.description || "";
metadata.latest_version = version;
metadata.versions = Array.from(new Set([...(metadata.versions || []), version]));
metadata.extra = Object.assign({}, metadata.extra || {}, localMeta.extra || {});
} else {
let curPath = process.cwd();
const metaFilePath = path.resolve( curPath, "meta.json" );
if ( ! fs.existsSync( metaFilePath ) ) {
$logger.error("当前路径不包含 meta.json")
return
}
const metaFile = fs.readFileSync(metaFilePath);
const localMeta = JSON5.parse( metaFile );
// 更新版本
const date = new Date();
localMeta.version = `${localMeta.version}.${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, '0')}${date.getDate().toString().padStart(2, '0')}${date.getHours().toString().padStart(2, '0')}${date.getMinutes().toString().padStart(2, '0')}${date.getSeconds().toString().padStart(2, '0')}`;
// 确认目录
// const directoryItems = await remote.getDirectoryContents("/modules");
// const modulesFolder = remote.getFileInfoByName(directoryItems, localMeta.id)
if ( ! await remote.client.exists("/modules/" + localMeta.id) ) {
// 创建文件夹
await remote.client.createDirectory("/modules/" + localMeta.id);
}
// 先更新文件
for (let dist of localMeta.dist ) {
const folder = "/modules/" + localMeta.id + "/" + localMeta.version + "/" + dist.os + "/" + dist.platform
await remote.wMkdir( folder );
$logger.info("upload dist", dist.path, fs.existsSync(dist.path))
await remote.wUpload( dist.path , folder + "/" + dist.filename );
}
// remote meta
let remoteMeta = {
"versions" : []
}
try {
const remoteMetaStr = await remote.client.getFileContents("/modules/" + localMeta.id + "/meta.json", { format: "text" });
remoteMeta = JSON5.parse( remoteMetaStr )
} catch (e) {
}
remoteMeta.id = localMeta.id;
remoteMeta.name = localMeta.name;
remoteMeta.desc = localMeta.desc;
remoteMeta.type = localMeta.type;
remoteMeta.vcs = localMeta.vcs;
// 废弃
// remoteMeta.platform = "cross" === localMeta.platform ? ["X86_64", "ARM64"] : localMeta.platform;
// remoteMeta.os = "cross" === localMeta.os ? ["Windows", "Linux", "Alpine"] : localMeta.os;
const dist = objutil.copy(localMeta.dist)
for (let d of dist) {
delete dist.path
}
remoteMeta.latest = {
version : localMeta.version,
dist
}
remoteMeta.versions.push(remoteMeta.latest)
if ( remoteMeta.versions.length >= 20 ) {
remoteMeta.versions = remoteMeta.versions.slice(0, 20);
}
await remote.client.putFileContents("/modules/" + localMeta.id + "/meta.json", JSON5.stringify(remoteMeta, null, 4) , { overwrite: true });
}
$logger.info("文件上传成功")
}
await remote.putJson(`/${projectId}/metadata.json`, metadata);
$logger.info(`文件上传成功 ${projectId}@${version}`);
};

Binary file not shown.

13
kit/test/remote_debug.js Normal file
View File

@ -0,0 +1,13 @@
require("../src/init")
const Remote = require("../src/remote")
async function main(){
const remote = new Remote();
await remote.init()
console.log(await remote.check())
}
main()

100
kit/test/remote_test.js Normal file
View File

@ -0,0 +1,100 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import fs from "fs";
import os from "os";
import path from "path";
require("../src/init")
async function createRemote() {
const Remote = require("../src/remote")
const remote = new Remote();
await remote.init()
return remote
}
let tempDir;
let localStore;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.resolve(os.tmpdir(), "kit-remote-test"));
localStore = $sConfig.getLocalStorePath();
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe("remote module public API", () => {
test("exposes documented public methods", async() => {
const remote = await createRemote();
expect(typeof remote.check).toBe("function");
expect(typeof remote.publish).toBe("function");
expect(typeof remote.install).toBe("function");
expect(typeof remote.list).toBe("function");
expect(typeof remote.use).toBe("function");
expect(typeof remote.rollback).toBe("function");
expect(typeof remote.remove).toBe("function");
});
test("check validates an available webdav client", async () => {
const remote = await createRemote();
expect(await remote.check()).toBe(true);
});
test("publish stores metadata and version artifacts in webdav layout", async () => {
const remote = await createRemote();
const projectDir = writeFixtureProject(tempDir, "1.0.0", "hello v1");
await remote.publish(projectDir);
const metadata = JSON.parse(await remote.client.getFileContents("/myapp/metadata.json", { format: "text" }));
const version = JSON.parse(await remote.client.getFileContents("/myapp/1.0.0/version.json", { format: "text" }));
const artifact = await remote.client.getFileContents("/myapp/1.0.0/any/any/myapp.txt", { format: "text" });
expect(metadata.project_id).toBe("myapp");
expect(metadata.latest_version).toBe("1.0.0");
expect(metadata.versions).toContain("1.0.0");
expect(version.artifacts[0].path).toBe("any/any/myapp.txt");
expect(artifact).toBe("hello v1");
});
test("install downloads latest matching artifact and records local state", async () => {
const remote = await createRemote();
const projectDir = writeFixtureProject(tempDir, "1.0.0", "hello v1");
await remote.publish(projectDir);
const installed = await remote.install("myapp");
const moduleRoot = path.resolve(localStore, "modules", "myapp");
const installJson = JSON.parse(fs.readFileSync(path.resolve(moduleRoot, ".meta", "install.json"), "utf-8"));
const installedFile = fs.readFileSync(path.resolve(moduleRoot, "versions", "1.0.0", "any", "any", "myapp.txt"), "utf-8");
expect(installed).toBe("1.0.0");
expect(installedFile).toBe("hello v1");
expect(installJson.current_version).toBe("1.0.0");
expect(installJson.installed_versions).toContain("1.0.0");
expect(fs.existsSync(path.resolve(moduleRoot, "current"))).toBe(true);
});
test("list use rollback and remove manage installed versions", async () => {
const remote = await createRemote();
await remote.publish(writeFixtureProject(tempDir, "1.0.0", "hello v1"));
await remote.install("myapp");
await remote.publish(writeFixtureProject(tempDir, "2.0.0", "hello v2"));
await remote.install("myapp");
expect(await remote.list("myapp")).toEqual(["1.0.0", "2.0.0"]);
await remote.rollback("myapp");
let installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("1.0.0");
await remote.use("myapp@2.0.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.current_version).toBe("2.0.0");
await remote.remove("myapp@1.0.0");
installJson = JSON.parse(fs.readFileSync(path.resolve(localStore, "modules", "myapp", ".meta", "install.json"), "utf-8"));
expect(installJson.installed_versions).toEqual(["2.0.0"]);
expect(fs.existsSync(path.resolve(localStore, "modules", "myapp", "versions", "1.0.0"))).toBe(false);
});
});

View File

@ -105,10 +105,10 @@ async function main(){
// }
// );
const client = webdav.createClient(
"http://baishe.fullstack.club:5244/dav",
"http://openlist.honor3.com/dav",
{
username: "kit",
password: "kitkit"
username: "sunyard",
password: "sunyard"
}
);
@ -136,9 +136,12 @@ async function main(){
// console.log("end")
await wUpload( client, "D:\\fullstack\\TDevOps\\Readme.md", "/test/case1/data/20250313111818/Readme.md2" )
// await wUpload( client, "D:\\fullstack\\TDevOps\\Readme.md", "/test/case1/data/20250313111818/Readme.md2" )
// await wUpload( client, "../dist/windows/kit.exe", "/modules/kit/0.1.20240227094429/Windows/X86_64/kit.exe" )
console.log("over")
}
//kit3 config remoteAuth sunyard:sunyard
//kit3 config remoteServer http://openlist.honor3.com/dav
main()