dxkj 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/cli.js +17 -6
- package/src/lib/installer.js +72 -1
- package/src/lib/registry.js +6 -3
- package/src/lib/version.js +8 -0
package/README.md
CHANGED
|
@@ -40,6 +40,16 @@ dxkj list --source ./node_modules/dxkj/fixtures/sample-registry.json
|
|
|
40
40
|
| `navlink` | 装入目标 NavLink 实例的 `plugins/` 目录 | `--dir` 或 `config.targets.navlink.pluginsDir` |
|
|
41
41
|
| `generic` | 解压到任意目录 | `--dir` 或 `config.targets.generic.dir` |
|
|
42
42
|
|
|
43
|
+
## 三种安装源格式(install_format)
|
|
44
|
+
|
|
45
|
+
| 格式 | 行为 | 需要 |
|
|
46
|
+
| :--- | :--- | :--- |
|
|
47
|
+
| `zip` | 从本站下载 zip 解压 | 本站托管安装包 |
|
|
48
|
+
| `url` | 下载外链;是 zip 则解压,否则作为单文件落盘 | 外部直链(CDN / GitHub Release / npm tarball) |
|
|
49
|
+
| `git` | `git clone --depth 1`(可指定 ref),拷贝子目录到目标 | 本机已装 git |
|
|
50
|
+
|
|
51
|
+
安装包不必都放本站:`url` / `git` 可把重文件外置到外部,npm 只承载瘦 CLI。
|
|
52
|
+
|
|
43
53
|
## 配置
|
|
44
54
|
|
|
45
55
|
配置文件:`~/.dxkj/config.json`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -6,9 +6,8 @@ import { loadRegistry, filterItems, findByRef, resolveSource } from './lib/regis
|
|
|
6
6
|
import { validateItem, formatReport } from './lib/validator.js';
|
|
7
7
|
import { renderList, renderDetail } from './lib/format.js';
|
|
8
8
|
import { resolveTargetDir } from './lib/targets.js';
|
|
9
|
-
import { downloadToTemp, extractZip, writeManifest, cleanup } from './lib/installer.js';
|
|
10
|
-
|
|
11
|
-
const VERSION = '0.1.0';
|
|
9
|
+
import { downloadToTemp, extractZip, writeManifest, cleanup, installFromUrl, installFromGit } from './lib/installer.js';
|
|
10
|
+
import { VERSION } from './lib/version.js';
|
|
12
11
|
|
|
13
12
|
const OPTIONS = {
|
|
14
13
|
source: { type: 'string' },
|
|
@@ -145,9 +144,20 @@ async function cmdInstall(argv) {
|
|
|
145
144
|
continue;
|
|
146
145
|
}
|
|
147
146
|
let temp = null;
|
|
147
|
+
const fmt = item.install.format || 'zip';
|
|
148
148
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
149
|
+
let detail = fmt;
|
|
150
|
+
if (fmt === 'git') {
|
|
151
|
+
const r = await installFromGit(item.install.url, item.install.ref, item.install.subdir, dest.dir);
|
|
152
|
+
detail = `git${r.commit ? ' @' + r.commit : ''}`;
|
|
153
|
+
} else if (fmt === 'url') {
|
|
154
|
+
const r = await installFromUrl(item.install.url, dest.dir);
|
|
155
|
+
detail = `url/${r.mode}`;
|
|
156
|
+
} else {
|
|
157
|
+
temp = await downloadToTemp(item.install.url);
|
|
158
|
+
extractZip(temp, dest.dir);
|
|
159
|
+
detail = 'zip';
|
|
160
|
+
}
|
|
151
161
|
writeManifest(dest.dir, {
|
|
152
162
|
id: item.id,
|
|
153
163
|
number: item.number,
|
|
@@ -155,10 +165,11 @@ async function cmdInstall(argv) {
|
|
|
155
165
|
kind: item.kind,
|
|
156
166
|
version: item.version,
|
|
157
167
|
source: item.install.url,
|
|
168
|
+
format: fmt,
|
|
158
169
|
target: dest.target,
|
|
159
170
|
installedAt: new Date().toISOString()
|
|
160
171
|
});
|
|
161
|
-
console.log(`✓ 已安装 #${item.number} ${item.name} → ${dest.dir} (${dest.hint})`);
|
|
172
|
+
console.log(`✓ 已安装 #${item.number} ${item.name} → ${dest.dir} (${detail} · ${dest.hint})`);
|
|
162
173
|
ok++;
|
|
163
174
|
} catch (err) {
|
|
164
175
|
console.error(`✗ #${item.number} ${item.name}: ${err.message}`);
|
package/src/lib/installer.js
CHANGED
|
@@ -2,7 +2,12 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
5
7
|
import AdmZip from 'adm-zip';
|
|
8
|
+
import { USER_AGENT } from './version.js';
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
6
11
|
|
|
7
12
|
export const MANIFEST = '.dxkj.json';
|
|
8
13
|
|
|
@@ -12,7 +17,7 @@ export async function downloadToTemp(url) {
|
|
|
12
17
|
const target = path.join(tmp, 'package.zip');
|
|
13
18
|
|
|
14
19
|
if (/^https?:\/\//i.test(url)) {
|
|
15
|
-
const res = await fetch(url, { headers: { 'User-Agent':
|
|
20
|
+
const res = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
|
16
21
|
if (!res.ok) throw new Error(`下载失败 HTTP ${res.status}:${url}`);
|
|
17
22
|
await pipeline(res.body, fs.createWriteStream(target));
|
|
18
23
|
} else {
|
|
@@ -66,3 +71,69 @@ export function cleanup(tempFile) {
|
|
|
66
71
|
/* ignore */
|
|
67
72
|
}
|
|
68
73
|
}
|
|
74
|
+
|
|
75
|
+
/** 是否为 zip 包(按魔数判断) */
|
|
76
|
+
export function isZipBuffer(buf) {
|
|
77
|
+
return buf.length >= 4 && buf[0] === 0x50 && buf[1] === 0x4b && (buf[2] === 0x03 || buf[2] === 0x05 || buf[2] === 0x07);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function copyDir(src, dest, { exclude = [] } = {}) {
|
|
81
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
82
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
83
|
+
if (exclude.includes(entry.name)) continue;
|
|
84
|
+
const s = path.join(src, entry.name);
|
|
85
|
+
const d = path.join(dest, entry.name);
|
|
86
|
+
if (entry.isDirectory()) copyDir(s, d, { exclude });
|
|
87
|
+
else fs.copyFileSync(s, d);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* format=url:下载外链,zip 则解压,否则作为单文件落盘
|
|
93
|
+
*/
|
|
94
|
+
export async function installFromUrl(url, destDir) {
|
|
95
|
+
const temp = await downloadToTemp(url);
|
|
96
|
+
try {
|
|
97
|
+
const buf = fs.readFileSync(temp);
|
|
98
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
99
|
+
if (isZipBuffer(buf)) {
|
|
100
|
+
extractZip(temp, destDir);
|
|
101
|
+
return { mode: 'zip' };
|
|
102
|
+
}
|
|
103
|
+
const name = decodeURIComponent((url.split('/').pop() || 'file.bin').split('?')[0]) || 'file.bin';
|
|
104
|
+
fs.copyFileSync(temp, path.join(destDir, name));
|
|
105
|
+
return { mode: 'file', filename: name };
|
|
106
|
+
} finally {
|
|
107
|
+
cleanup(temp);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* format=git:clone 仓库(浅克隆),拷贝子目录/根目录到目标(排除 .git)
|
|
113
|
+
*/
|
|
114
|
+
export async function installFromGit(repo, ref, subdir, destDir) {
|
|
115
|
+
const tmp = path.join(os.tmpdir(), `dxkj-git-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
116
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
117
|
+
const args = ['clone', '--depth', '1'];
|
|
118
|
+
if (ref) args.push('--branch', ref);
|
|
119
|
+
args.push(repo, tmp);
|
|
120
|
+
try {
|
|
121
|
+
await execFileAsync('git', args, { timeout: 180000 });
|
|
122
|
+
} catch (e) {
|
|
123
|
+
const msg = (e.stderr ? String(e.stderr).trim().split('\n').slice(-1)[0] : e.message) || '未知错误';
|
|
124
|
+
throw new Error(`git clone 失败:${msg}(请确认已安装 git 且仓库可访问)`);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const src = subdir ? path.join(tmp, subdir) : tmp;
|
|
128
|
+
if (!fs.existsSync(src)) throw new Error(`子目录不存在:${subdir}`);
|
|
129
|
+
copyDir(src, destDir, { exclude: subdir ? [] : ['.git'] });
|
|
130
|
+
let commit = '';
|
|
131
|
+
try {
|
|
132
|
+
const { stdout } = await execFileAsync('git', ['-C', tmp, 'rev-parse', '--short', 'HEAD'], { timeout: 20000 });
|
|
133
|
+
commit = stdout.trim();
|
|
134
|
+
} catch { /* ignore */ }
|
|
135
|
+
return { mode: 'git', commit };
|
|
136
|
+
} finally {
|
|
137
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/lib/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { loadConfig, resolveServerUrl, REGISTRY_PATH } from './config.js';
|
|
4
|
+
import { USER_AGENT } from './version.js';
|
|
4
5
|
|
|
5
6
|
/** 去除 Windows 写文件常见的 UTF-8 BOM,避免 JSON.parse 失败 */
|
|
6
7
|
export function stripBom(text) {
|
|
@@ -34,8 +35,10 @@ function normalizeItem(raw, index) {
|
|
|
34
35
|
images: Array.isArray(raw.images) ? raw.images : [],
|
|
35
36
|
install: {
|
|
36
37
|
format: install.format || raw.install_format || 'zip',
|
|
37
|
-
url: install.url || raw.downloadUrl || raw.package_url || '',
|
|
38
|
-
entry: install.entry || raw.entry || ''
|
|
38
|
+
url: install.url || raw.downloadUrl || raw.package_url || raw.install_url || '',
|
|
39
|
+
entry: install.entry || raw.entry || '',
|
|
40
|
+
ref: install.ref || raw.install_ref || '',
|
|
41
|
+
subdir: install.subdir || raw.install_subdir || ''
|
|
39
42
|
},
|
|
40
43
|
raw
|
|
41
44
|
};
|
|
@@ -52,7 +55,7 @@ export async function loadRegistry(opts = {}, cfg = loadConfig()) {
|
|
|
52
55
|
try {
|
|
53
56
|
const res = await fetch(source, {
|
|
54
57
|
signal: controller.signal,
|
|
55
|
-
headers: { 'User-Agent':
|
|
58
|
+
headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' }
|
|
56
59
|
});
|
|
57
60
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
58
61
|
json = await res.json();
|