@asiainfo-sdd/multica-cli 0.2.1 → 0.3.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.
- package/README.md +129 -98
- package/bin/multica-uninstall.js +5 -0
- package/bin/multica.js +5 -8
- package/lib/dist-config.json +2 -1
- package/lib/http.js +174 -174
- package/lib/install.js +337 -333
- package/lib/platform.js +45 -45
- package/lib/server-config.js +102 -102
- package/lib/uninstall.js +263 -0
- package/package.json +7 -4
- package/bin/aido.js +0 -52
package/lib/install.js
CHANGED
|
@@ -1,333 +1,337 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// postinstall 主流程:按平台下载 multica CLI 二进制(-tags agentfoundry
|
|
4
|
-
// 构建,含 daemon 主机线能力)并落位到包内 vendor/。
|
|
5
|
-
// 只管安装:不读写 ~/.multica 配置、不登录、不启动/停止 daemon。
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const crypto = require('crypto');
|
|
10
|
-
const { spawnSync } = require('child_process');
|
|
11
|
-
|
|
12
|
-
const { detectPlatform, manifestKey, vendorBinaryPath } = require('./platform');
|
|
13
|
-
const { applyServerConfig } = require('./server-config');
|
|
14
|
-
const http = require('./http');
|
|
15
|
-
|
|
16
|
-
const pkg = require('../package.json');
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function
|
|
35
|
-
return path.join(
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
log('
|
|
319
|
-
log(' 1.
|
|
320
|
-
log(' 2.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
log(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// postinstall 主流程:按平台下载 multica CLI 二进制(-tags agentfoundry
|
|
4
|
+
// 构建,含 daemon 主机线能力)并落位到包内 vendor/。
|
|
5
|
+
// 只管安装:不读写 ~/.multica 配置、不登录、不启动/停止 daemon。
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const { detectPlatform, manifestKey, vendorBinaryPath } = require('./platform');
|
|
13
|
+
const { applyServerConfig } = require('./server-config');
|
|
14
|
+
const http = require('./http');
|
|
15
|
+
|
|
16
|
+
const pkg = require('../package.json');
|
|
17
|
+
|
|
18
|
+
// 下载源兜底基址(生产)。实际取值:MULTICA_DOWNLOAD_BASE 环境变量
|
|
19
|
+
// > lib/dist-config.json 的 download_base(发布脚本按 profile 注入)> 此默认值。
|
|
20
|
+
const DEFAULT_DOWNLOAD_BASE = 'https://aido.asiainfo.com/aird-api/business/api/v1/external/daemon-download';
|
|
21
|
+
const LOCK_STALE_MS = 5 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
function log(msg) {
|
|
24
|
+
process.stdout.write(`${msg}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function downloadBase() {
|
|
28
|
+
const fromConfig = require('./dist-config.json').download_base;
|
|
29
|
+
return (
|
|
30
|
+
process.env.MULTICA_DOWNLOAD_BASE || fromConfig || DEFAULT_DOWNLOAD_BASE
|
|
31
|
+
).replace(/\/+$/, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function vendorDir() {
|
|
35
|
+
return path.join(__dirname, '..', 'vendor');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installedMarkerPath() {
|
|
39
|
+
return path.join(vendorDir(), '.installed.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readInstalled() {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(installedMarkerPath(), 'utf8'));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------- manifest ----------
|
|
51
|
+
|
|
52
|
+
function fallbackManifest() {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(
|
|
55
|
+
fs.readFileSync(path.join(__dirname, 'manifest.fallback.json'), 'utf8'),
|
|
56
|
+
);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function loadManifest() {
|
|
63
|
+
const base = downloadBase();
|
|
64
|
+
try {
|
|
65
|
+
const text = await http.fetchText(`${base}/manifest.json`, { timeoutMs: 10000 });
|
|
66
|
+
return { manifest: JSON.parse(text), source: base };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
log(`⚠ 无法从 ${base} 获取 manifest(${err.message}),改用包内置兜底清单`);
|
|
69
|
+
const fb = fallbackManifest();
|
|
70
|
+
if (fb) return { manifest: fb, source: 'fallback' };
|
|
71
|
+
return { manifest: null, source: 'none' };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pickEntry(manifest, version) {
|
|
76
|
+
return manifest?.versions?.[version] || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------- 锁 ----------
|
|
80
|
+
|
|
81
|
+
function acquireLock(dir) {
|
|
82
|
+
const lockPath = path.join(dir, '.lock');
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
84
|
+
for (let attempt = 0; ; attempt++) {
|
|
85
|
+
try {
|
|
86
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
87
|
+
fs.writeSync(fd, String(process.pid));
|
|
88
|
+
fs.closeSync(fd);
|
|
89
|
+
return { release: () => fs.rmSync(lockPath, { force: true }) };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.code !== 'EEXIST') throw err;
|
|
92
|
+
try {
|
|
93
|
+
const st = fs.statSync(lockPath);
|
|
94
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
95
|
+
fs.rmSync(lockPath, { force: true });
|
|
96
|
+
continue; // 陈旧锁已破,重试获取
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
continue; // 锁文件刚被对方释放
|
|
100
|
+
}
|
|
101
|
+
if (attempt > 60) {
|
|
102
|
+
throw new Error('另一进程正在下载 multica 二进制,等待超时;请稍后重试 npm rebuild');
|
|
103
|
+
}
|
|
104
|
+
const until = Date.now() + 2000;
|
|
105
|
+
while (Date.now() < until) {} // 同步安装脚本内简短等待
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------- 下载与校验 ----------
|
|
111
|
+
|
|
112
|
+
function progressPrinter(isTTY) {
|
|
113
|
+
if (!isTTY) {
|
|
114
|
+
let announced = false;
|
|
115
|
+
return (done, total) => {
|
|
116
|
+
if (!announced) {
|
|
117
|
+
announced = true;
|
|
118
|
+
log(`… 开始下载${total ? `(共 ${(total / 1048576).toFixed(1)} MB)` : ''},完成后报告`);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return (done, total) => {
|
|
123
|
+
const mb = (n) => (n / 1048576).toFixed(1);
|
|
124
|
+
const pct = total ? ` (${Math.floor((done / total) * 100)}%)` : '';
|
|
125
|
+
process.stdout.write(`\r 下载中 ${mb(done)}${total ? `/${mb(total)} MB` : ' MB'}${pct} `);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sha256Of(filePath) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const hash = crypto.createHash('sha256');
|
|
132
|
+
const stream = fs.createReadStream(filePath);
|
|
133
|
+
stream.on('error', reject);
|
|
134
|
+
stream.on('data', (c) => hash.update(c));
|
|
135
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function downloadOnce(url, tmpPath, expectedSize) {
|
|
140
|
+
await new Promise((resolve, reject) => {
|
|
141
|
+
fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
|
|
142
|
+
const out = fs.createWriteStream(tmpPath);
|
|
143
|
+
const onProgress = progressPrinter(process.stdout.isTTY);
|
|
144
|
+
http
|
|
145
|
+
.downloadToStream(url, out, { onProgress })
|
|
146
|
+
.then(resolve, reject);
|
|
147
|
+
});
|
|
148
|
+
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
149
|
+
if (expectedSize && fs.statSync(tmpPath).size !== expectedSize) {
|
|
150
|
+
throw new Error(`大小不符:期望 ${expectedSize},实际 ${fs.statSync(tmpPath).size}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hintRunRebuild() {
|
|
155
|
+
log(` 修复方式:稍后执行 npm rebuild ${pkg.name}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 把部署级配置(server_url/app_url)写入 ~/.multica/config.json(覆盖式)。
|
|
159
|
+
// manifest 失败时传 null,退回 env > 包内置默认值。
|
|
160
|
+
function applyServerConfigSafe(manifest) {
|
|
161
|
+
let result;
|
|
162
|
+
try {
|
|
163
|
+
result = applyServerConfig(manifest);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log(`⚠ 写入服务器配置失败(${err.message}),请手工执行 multica config set server_url <地址>`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!result) return;
|
|
169
|
+
for (const { key, value } of result.written) {
|
|
170
|
+
log(`✓ 已写入 ${key} = ${value}(${require('./server-config').multicaConfigPath()})`);
|
|
171
|
+
}
|
|
172
|
+
for (const { key, from, to } of result.updated) {
|
|
173
|
+
log(`✓ 已更新 ${key}:${from} → ${to}(环境切换;token 需重新登录生成)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function install() {
|
|
178
|
+
if (process.env.MULTICA_SKIP_BINARY_INSTALL === '1') {
|
|
179
|
+
log('ℹ MULTICA_SKIP_BINARY_INSTALL=1,跳过二进制下载。');
|
|
180
|
+
log(` 需要时执行 npm rebuild ${pkg.name} 完成下载。`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const plat = detectPlatform();
|
|
185
|
+
if (!plat) {
|
|
186
|
+
log(`⚠ 当前平台 ${process.platform}/${process.arch} 不在支持矩阵(darwin/linux/windows × amd64/arm64),跳过下载。`);
|
|
187
|
+
log(' 如需在其他机器安装,请确认平台受支持后重试。');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const version = pkg.version;
|
|
192
|
+
const key = manifestKey(plat);
|
|
193
|
+
const { manifest } = await loadManifest();
|
|
194
|
+
const versionEntry = pickEntry(manifest, version);
|
|
195
|
+
const entry = versionEntry?.[key];
|
|
196
|
+
|
|
197
|
+
if (!entry) {
|
|
198
|
+
log(`⚠ 下载清单中未找到 v${version} 的 ${key} 产物,暂不下载。`);
|
|
199
|
+
log(` 下载源:${downloadBase()}(可用 MULTICA_DOWNLOAD_BASE 覆盖)`);
|
|
200
|
+
hintRunRebuild();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const dir = vendorDir();
|
|
205
|
+
const target = vendorBinaryPath(dir, plat);
|
|
206
|
+
|
|
207
|
+
// 幂等:已装同版本且校验通过则跳过。
|
|
208
|
+
const installed = readInstalled();
|
|
209
|
+
if (
|
|
210
|
+
installed &&
|
|
211
|
+
installed.version === version &&
|
|
212
|
+
installed.sha256 === entry.sha256 &&
|
|
213
|
+
fs.existsSync(target)
|
|
214
|
+
) {
|
|
215
|
+
log(`✓ multica v${version} 已就绪(${path.basename(target)},无需重复下载)`);
|
|
216
|
+
applyServerConfigSafe(manifest);
|
|
217
|
+
printUsage(version);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const url = `${downloadBase()}/${entry.path.replace(/^\/+/, '')}`;
|
|
222
|
+
log(`→ 安装 multica(daemon 客户端)v${version}(${key})`);
|
|
223
|
+
log(` 下载源:${url}`);
|
|
224
|
+
|
|
225
|
+
const lock = acquireLock(dir);
|
|
226
|
+
try {
|
|
227
|
+
// 拿到锁后复查:等锁期间对方可能已完成安装。
|
|
228
|
+
const recheck = readInstalled();
|
|
229
|
+
if (
|
|
230
|
+
recheck &&
|
|
231
|
+
recheck.version === version &&
|
|
232
|
+
recheck.sha256 === entry.sha256 &&
|
|
233
|
+
fs.existsSync(target)
|
|
234
|
+
) {
|
|
235
|
+
log(`✓ multica v${version} 已就绪(并发安装合并,跳过下载)`);
|
|
236
|
+
printUsage(version);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const tmpPath = path.join(dir, `.tmp-${process.pid}`);
|
|
241
|
+
let lastErr = null;
|
|
242
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
243
|
+
try {
|
|
244
|
+
await downloadOnce(url, tmpPath, entry.size);
|
|
245
|
+
const actual = await sha256Of(tmpPath);
|
|
246
|
+
if (actual.toLowerCase() !== entry.sha256.toLowerCase()) {
|
|
247
|
+
throw new Error(`sha256 校验失败:期望 ${entry.sha256},实际 ${actual}`);
|
|
248
|
+
}
|
|
249
|
+
lastErr = null;
|
|
250
|
+
break;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
lastErr = err;
|
|
253
|
+
fs.rmSync(tmpPath, { force: true });
|
|
254
|
+
if (attempt === 1) log(` 下载/校验失败(${err.message}),重试一次…`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (lastErr) {
|
|
258
|
+
log(`✗ 二进制下载失败:${lastErr.message}`);
|
|
259
|
+
log(` 请检查网络与下载源 ${downloadBase()} 是否可达(代理变量 http_proxy/https_proxy 会被识别)`);
|
|
260
|
+
hintRunRebuild();
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
fs.rmSync(target, { force: true });
|
|
267
|
+
fs.renameSync(tmpPath, target);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (err.code === 'EPERM' || err.code === 'EBUSY' || err.code === 'EACCES') {
|
|
270
|
+
log('✗ 无法替换二进制文件:文件被占用(通常是 daemon 正在运行)。');
|
|
271
|
+
log(` 请先执行 multica daemon stop,再运行 npm rebuild ${pkg.name}。`);
|
|
272
|
+
} else {
|
|
273
|
+
log(`✗ 落位失败:${err.message}`);
|
|
274
|
+
}
|
|
275
|
+
hintRunRebuild();
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (process.platform !== 'win32') fs.chmodSync(target, 0o755);
|
|
281
|
+
if (process.platform === 'darwin') {
|
|
282
|
+
// ad-hoc 签名,避免 Gatekeeper 拦截 spawn;失败不致命。
|
|
283
|
+
const r = spawnSync('codesign', ['-s', '-', '--force', target], { stdio: 'ignore' });
|
|
284
|
+
if (r.status !== 0) log(' (codesign ad-hoc 签名失败,可忽略)');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fs.writeFileSync(
|
|
288
|
+
installedMarkerPath(),
|
|
289
|
+
JSON.stringify(
|
|
290
|
+
{ version, sha256: entry.sha256, installedAt: new Date().toISOString() },
|
|
291
|
+
null,
|
|
292
|
+
2,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
log(`✓ multica v${version} 安装完成:${target}`);
|
|
296
|
+
applyServerConfigSafe(manifest);
|
|
297
|
+
printUsage(version);
|
|
298
|
+
} finally {
|
|
299
|
+
lock.release();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function printUsage(version) {
|
|
304
|
+
log('');
|
|
305
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
306
|
+
log(` multica(daemon 客户端)v${version} 已就绪`);
|
|
307
|
+
const configPath = require('./server-config').multicaConfigPath();
|
|
308
|
+
// 以用户配置文件里最终的 server_url 为准(含此前手工配置过的场景)。
|
|
309
|
+
let effectiveServerUrl = '';
|
|
310
|
+
try {
|
|
311
|
+
effectiveServerUrl =
|
|
312
|
+
JSON.parse(require('fs').readFileSync(configPath, 'utf8')).server_url || '';
|
|
313
|
+
} catch {
|
|
314
|
+
// 文件缺失/损坏:走三步指引
|
|
315
|
+
}
|
|
316
|
+
if (effectiveServerUrl) {
|
|
317
|
+
log(` ✓ 服务器地址已就绪:${configPath}(server_url = ${effectiveServerUrl})`);
|
|
318
|
+
log(' 接下来请自行完成两步:');
|
|
319
|
+
log(' 1. multica login --token mul_<个人令牌> ← 令牌在 Web 端“设置→令牌”生成');
|
|
320
|
+
log(' 2. multica daemon start ← 需本机已装至少一个 agent CLI(claude/codex/opencode…)');
|
|
321
|
+
} else {
|
|
322
|
+
log(' 接下来请自行完成三步:');
|
|
323
|
+
log(' 1. multica config set server_url https://<服务器地址>');
|
|
324
|
+
log(' 2. multica login --token mul_<个人令牌> ← 令牌在 Web 端“设置→令牌”生成');
|
|
325
|
+
log(' 3. multica daemon start ← 需本机已装至少一个 agent CLI(claude/codex/opencode…)');
|
|
326
|
+
}
|
|
327
|
+
log(' 日常管理:multica daemon status / logs / stop');
|
|
328
|
+
log(' 提示“multica 不是命令” → 重新打开终端,或用 npx multica ...');
|
|
329
|
+
log(' 本包只负责安装与部署级配置:不会写 token、不会登录、不会启动 daemon。');
|
|
330
|
+
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
install().catch((err) => {
|
|
334
|
+
log(`✗ 安装脚本异常:${err.stack || err.message}`);
|
|
335
|
+
hintRunRebuild();
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
});
|