@yixinkj/cli 1.0.1 → 1.0.2

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.
Files changed (3) hide show
  1. package/README.md +10 -6
  2. package/index.js +173 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,16 +36,20 @@ YIXIN_AUTH_URL=https://你的授权服务域名/v1/token
36
36
  1. 从环境变量读取 `YIXIN_KEY`。
37
37
  2. 向 `YIXIN_AUTH_URL` 提交 `{ key, skill: "ads", version }`。
38
38
  3. 获取短期授权 token。
39
- 4. 使用以下参数运行 `~/.yixin/bin/alibaba-cli-{platform}`:
39
+ 4. 如当前版本二进制尚未安装,则从公开 GitHub Release 下载对应平台包并解压到 `~/.yixin/bin`。
40
+ 5. 使用以下参数运行 `~/.yixin/bin/alibaba-cli-{platform}`:
40
41
 
41
42
  ```text
42
43
  --ads --period <period>
43
44
  ```
44
45
 
45
- 5. 将原生二进制的 stdout 返回给 MCP 客户端。
46
+ 6. 将原生二进制的 stdout 返回给 MCP 客户端。
46
47
 
47
- ## 本地二进制名称
48
+ ## 二进制发布包
48
49
 
49
- - `darwin-arm64` -> `~/.yixin/bin/alibaba-cli-mac-arm64`
50
- - `darwin-x64` -> `~/.yixin/bin/alibaba-cli-mac-x64`
51
- - `win32-x64` -> `~/.yixin/bin/alibaba-cli-win-x64.exe`
50
+ 默认从 `yixinkj/cli-releases` 下载与 npm 包版本一致的 release asset:
51
+
52
+ - `darwin-arm64` -> `yixin-mac-arm64.tar.gz`
53
+ - `win32-x64` -> `yixin-win-x64.zip`
54
+
55
+ 可通过 `YIXIN_RELEASE_REPO` 或 `YIXIN_RELEASE_BASE_URL` 覆盖下载来源。
package/index.js CHANGED
@@ -16,12 +16,24 @@ const require = createRequire(import.meta.url);
16
16
  const packageJson = require('./package.json');
17
17
 
18
18
  const VERSION = packageJson.version;
19
+ const RELEASE_REPO = process.env.YIXIN_RELEASE_REPO || 'yixinkj/cli-releases';
20
+ const CACHE_ROOT = path.join(os.homedir(), '.yixin');
19
21
  const BIN_DIR = path.join(os.homedir(), '.yixin', 'bin');
22
+ const VERSION_FILE = path.join(CACHE_ROOT, 'version');
20
23
 
21
- const PLATFORM_BINARIES = {
22
- 'darwin-arm64': 'alibaba-cli-mac-arm64',
23
- 'darwin-x64': 'alibaba-cli-mac-x64',
24
- 'win32-x64': 'alibaba-cli-win-x64.exe'
24
+ const PLATFORM_TARGETS = {
25
+ 'darwin-arm64': {
26
+ id: 'mac-arm64',
27
+ archive: 'yixin-mac-arm64.tar.gz',
28
+ mainBinary: 'alibaba-cli-mac-arm64',
29
+ binaries: ['alibaba-cli-mac-arm64', 'report-engine-mac-arm64', 'browser-bridge-mac-arm64']
30
+ },
31
+ 'win32-x64': {
32
+ id: 'win-x64',
33
+ archive: 'yixin-win-x64.zip',
34
+ mainBinary: 'alibaba-cli-win-x64.exe',
35
+ binaries: ['alibaba-cli-win-x64.exe', 'report-engine-win-x64.exe']
36
+ }
25
37
  };
26
38
 
27
39
  const PERIOD_ALIASES = {
@@ -47,25 +59,161 @@ const PERIOD_ALIASES = {
47
59
  'last-30-days': 'last-30-days'
48
60
  };
49
61
 
50
- function detectBinary() {
62
+ function detectTarget() {
51
63
  const platformKey = `${process.platform}-${process.arch}`;
52
- const binaryName = PLATFORM_BINARIES[platformKey];
53
- if (!binaryName) {
54
- throw new Error(`暂不支持当前平台:${process.platform}/${process.arch}。已支持:mac-arm64、mac-x64、win-x64。`);
64
+ const target = PLATFORM_TARGETS[platformKey];
65
+ if (!target) {
66
+ throw new Error(`暂不支持当前平台:${process.platform}/${process.arch}。已支持:mac-arm64、win-x64。`);
67
+ }
68
+ return target;
69
+ }
70
+
71
+ function binaryPath(target) {
72
+ return path.join(BIN_DIR, target.mainBinary);
73
+ }
74
+
75
+ function isInstalled(target) {
76
+ if (!fs.existsSync(VERSION_FILE)) {
77
+ return false;
78
+ }
79
+ if (fs.readFileSync(VERSION_FILE, 'utf8').trim() !== VERSION) {
80
+ return false;
55
81
  }
56
- return path.join(BIN_DIR, binaryName);
82
+ return target.binaries.every((name) => fs.existsSync(path.join(BIN_DIR, name)));
57
83
  }
58
84
 
59
- function ensureBinary(binaryPath) {
60
- if (!fs.existsSync(binaryPath)) {
61
- throw new Error(`未找到本地二进制:${binaryPath}`);
85
+ function releaseUrl(target) {
86
+ if (process.env.YIXIN_RELEASE_BASE_URL) {
87
+ return `${process.env.YIXIN_RELEASE_BASE_URL.replace(/\/$/, '')}/${target.archive}`;
62
88
  }
63
- if (process.platform !== 'win32') {
64
- try {
65
- fs.chmodSync(binaryPath, 0o755);
66
- } catch (_) {
67
- // 继续执行,让 spawnSync 在真正启动失败时返回具体错误。
89
+ return `https://github.com/${RELEASE_REPO}/releases/download/v${VERSION}/${target.archive}`;
90
+ }
91
+
92
+ function downloadFile(url, outputPath, redirectsLeft = 5) {
93
+ return new Promise((resolve, reject) => {
94
+ const requestUrl = new URL(url);
95
+ const headers = {
96
+ 'User-Agent': '@yixinkj/cli',
97
+ Accept: 'application/octet-stream'
98
+ };
99
+
100
+ if (process.env.YIXIN_GITHUB_TOKEN && requestUrl.hostname.endsWith('github.com')) {
101
+ headers.Authorization = `Bearer ${process.env.YIXIN_GITHUB_TOKEN}`;
102
+ }
103
+
104
+ https.get(url, { headers }, (response) => {
105
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
106
+ response.resume();
107
+ if (redirectsLeft <= 0) {
108
+ reject(new Error(`下载时重定向次数过多:${url}`));
109
+ return;
110
+ }
111
+ const location = response.headers.location;
112
+ if (!location) {
113
+ reject(new Error(`重定向响应缺少 Location:${url}`));
114
+ return;
115
+ }
116
+ const nextUrl = new URL(location, url).toString();
117
+ downloadFile(nextUrl, outputPath, redirectsLeft - 1).then(resolve, reject);
118
+ return;
119
+ }
120
+
121
+ if (response.statusCode !== 200) {
122
+ response.resume();
123
+ reject(new Error(`下载失败:HTTP ${response.statusCode} ${url}`));
124
+ return;
125
+ }
126
+
127
+ const file = fs.createWriteStream(outputPath);
128
+ response.pipe(file);
129
+ file.on('finish', () => {
130
+ file.close(resolve);
131
+ });
132
+ file.on('error', reject);
133
+ }).on('error', reject);
134
+ });
135
+ }
136
+
137
+ function runCommand(command, args) {
138
+ const result = spawnSync(command, args, {
139
+ encoding: 'utf8',
140
+ stdio: ['ignore', 'pipe', 'pipe']
141
+ });
142
+ if (result.error) {
143
+ throw result.error;
144
+ }
145
+ if (result.status !== 0) {
146
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
147
+ throw new Error(detail || `${command} 退出码为 ${result.status}`);
148
+ }
149
+ }
150
+
151
+ function extractArchive(archivePath, target) {
152
+ fs.rmSync(BIN_DIR, { recursive: true, force: true });
153
+ fs.mkdirSync(BIN_DIR, { recursive: true });
154
+
155
+ if (target.archive.endsWith('.tar.gz')) {
156
+ runCommand('tar', ['-xzf', archivePath, '-C', BIN_DIR]);
157
+ return;
158
+ }
159
+
160
+ if (target.archive.endsWith('.zip')) {
161
+ if (process.platform === 'win32') {
162
+ runCommand('powershell.exe', [
163
+ '-NoProfile',
164
+ '-ExecutionPolicy',
165
+ 'Bypass',
166
+ '-Command',
167
+ `Expand-Archive -LiteralPath ${JSON.stringify(archivePath)} -DestinationPath ${JSON.stringify(BIN_DIR)} -Force`
168
+ ]);
169
+ return;
170
+ }
171
+ runCommand('unzip', ['-q', archivePath, '-d', BIN_DIR]);
172
+ return;
173
+ }
174
+
175
+ throw new Error(`暂不支持的压缩包类型:${target.archive}`);
176
+ }
177
+
178
+ function chmodExecutables(target) {
179
+ if (process.platform === 'win32') {
180
+ return;
181
+ }
182
+ for (const name of target.binaries) {
183
+ const filePath = path.join(BIN_DIR, name);
184
+ if (fs.existsSync(filePath)) {
185
+ fs.chmodSync(filePath, 0o755);
186
+ }
187
+ }
188
+ }
189
+
190
+ async function ensureInstalled(target) {
191
+ if (isInstalled(target)) {
192
+ chmodExecutables(target);
193
+ return;
194
+ }
195
+
196
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'yixin-cli-'));
197
+ const archivePath = path.join(tempDir, target.archive);
198
+ const url = releaseUrl(target);
199
+
200
+ try {
201
+ console.error(`[yixin] 正在安装 ${target.id} 二进制,版本 ${VERSION}`);
202
+ console.error(`[yixin] 正在下载 ${url}`);
203
+ await downloadFile(url, archivePath);
204
+ extractArchive(archivePath, target);
205
+ chmodExecutables(target);
206
+
207
+ for (const name of target.binaries) {
208
+ const filePath = path.join(BIN_DIR, name);
209
+ if (!fs.existsSync(filePath)) {
210
+ throw new Error(`压缩包中缺少 ${name}`);
211
+ }
68
212
  }
213
+ fs.mkdirSync(CACHE_ROOT, { recursive: true });
214
+ fs.writeFileSync(VERSION_FILE, `${VERSION}\n`);
215
+ } finally {
216
+ fs.rmSync(tempDir, { recursive: true, force: true });
69
217
  }
70
218
  }
71
219
 
@@ -132,9 +280,10 @@ async function authorize(skill) {
132
280
  return auth.token;
133
281
  }
134
282
 
135
- function runNative({ binaryPath, args, token }) {
136
- ensureBinary(binaryPath);
137
- const result = spawnSync(binaryPath, args, {
283
+ async function runNative({ target, args, token }) {
284
+ await ensureInstalled(target);
285
+ const executablePath = binaryPath(target);
286
+ const result = spawnSync(executablePath, args, {
138
287
  encoding: 'utf8',
139
288
  env: {
140
289
  ...process.env,
@@ -147,7 +296,7 @@ function runNative({ binaryPath, args, token }) {
147
296
  }
148
297
  if (result.status !== 0) {
149
298
  const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
150
- throw new Error(detail || `${binaryPath} exited with code ${result.status}`);
299
+ throw new Error(detail || `${executablePath} exited with code ${result.status}`);
151
300
  }
152
301
  return result.stdout || '';
153
302
  }
@@ -159,8 +308,9 @@ async function runAdsReport({ period }) {
159
308
  }
160
309
 
161
310
  const token = await authorize('ads');
162
- const stdout = runNative({
163
- binaryPath: detectBinary(),
311
+ const target = detectTarget();
312
+ const stdout = await runNative({
313
+ target,
164
314
  args: ['--ads', '--period', normalizedPeriod],
165
315
  token
166
316
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yixinkj/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Yixin local MCP server and CLI launcher for MCP clients.",
5
5
  "type": "module",
6
6
  "bin": {