dxkj 0.1.2 → 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 +16 -4
- package/src/lib/installer.js +70 -0
- package/src/lib/registry.js +4 -2
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,7 +6,7 @@ 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';
|
|
9
|
+
import { downloadToTemp, extractZip, writeManifest, cleanup, installFromUrl, installFromGit } from './lib/installer.js';
|
|
10
10
|
import { VERSION } from './lib/version.js';
|
|
11
11
|
|
|
12
12
|
const OPTIONS = {
|
|
@@ -144,9 +144,20 @@ async function cmdInstall(argv) {
|
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
146
|
let temp = null;
|
|
147
|
+
const fmt = item.install.format || 'zip';
|
|
147
148
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
}
|
|
150
161
|
writeManifest(dest.dir, {
|
|
151
162
|
id: item.id,
|
|
152
163
|
number: item.number,
|
|
@@ -154,10 +165,11 @@ async function cmdInstall(argv) {
|
|
|
154
165
|
kind: item.kind,
|
|
155
166
|
version: item.version,
|
|
156
167
|
source: item.install.url,
|
|
168
|
+
format: fmt,
|
|
157
169
|
target: dest.target,
|
|
158
170
|
installedAt: new Date().toISOString()
|
|
159
171
|
});
|
|
160
|
-
console.log(`✓ 已安装 #${item.number} ${item.name} → ${dest.dir} (${dest.hint})`);
|
|
172
|
+
console.log(`✓ 已安装 #${item.number} ${item.name} → ${dest.dir} (${detail} · ${dest.hint})`);
|
|
161
173
|
ok++;
|
|
162
174
|
} catch (err) {
|
|
163
175
|
console.error(`✗ #${item.number} ${item.name}: ${err.message}`);
|
package/src/lib/installer.js
CHANGED
|
@@ -2,9 +2,13 @@ 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';
|
|
6
8
|
import { USER_AGENT } from './version.js';
|
|
7
9
|
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
8
12
|
export const MANIFEST = '.dxkj.json';
|
|
9
13
|
|
|
10
14
|
export async function downloadToTemp(url) {
|
|
@@ -67,3 +71,69 @@ export function cleanup(tempFile) {
|
|
|
67
71
|
/* ignore */
|
|
68
72
|
}
|
|
69
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
|
@@ -35,8 +35,10 @@ function normalizeItem(raw, index) {
|
|
|
35
35
|
images: Array.isArray(raw.images) ? raw.images : [],
|
|
36
36
|
install: {
|
|
37
37
|
format: install.format || raw.install_format || 'zip',
|
|
38
|
-
url: install.url || raw.downloadUrl || raw.package_url || '',
|
|
39
|
-
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 || ''
|
|
40
42
|
},
|
|
41
43
|
raw
|
|
42
44
|
};
|