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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dxkj",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "dxkj CLI — browse and install prompts/skills/agents/plugins/tools from the dxkj catalog, plus an opencode plugin shell.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
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
- temp = await downloadToTemp(item.install.url);
150
- extractZip(temp, dest.dir);
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}`);
@@ -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': 'dxkj-cli/0.1.0' } });
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
+ }
@@ -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': 'dxkj-cli/0.1.0', Accept: 'application/json' }
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();
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ /** 版本号唯一来源:package.json,避免与硬编码漂移 */
4
+ export const VERSION = JSON.parse(
5
+ readFileSync(new URL('../../package.json', import.meta.url), 'utf-8')
6
+ ).version;
7
+
8
+ export const USER_AGENT = `dxkj-cli/${VERSION}`;