@asiainfo-sdd/multica-cli-test 0.0.0-stage → 0.3.0
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 +102 -2
- package/bin/multica.js +49 -0
- package/lib/dist-config.json +5 -0
- package/lib/http.js +174 -0
- package/lib/install.js +337 -0
- package/lib/platform.js +45 -0
- package/lib/server-config.js +102 -0
- package/package.json +22 -4
package/README.md
CHANGED
|
@@ -1,3 +1,103 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @asiainfo-sdd/multica-cli — multica(daemon 客户端)安装帮助包
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
通过 npm 安装数字工厂 multica 命令(daemon 客户端二进制,含主机线能力)。
|
|
4
|
+
**本包负责安装二进制与部署级配置**:npm install 时按平台自动下载、sha256 校验、落位,
|
|
5
|
+
并把 `server_url` / `app_url` 合并写入 `~/.multica/config.json`(只填空,不覆盖已有值);
|
|
6
|
+
**不写 token、不登录、不启动/停止 daemon**——个人凭据与 daemon 生命周期始终由用户掌控。
|
|
7
|
+
|
|
8
|
+
## 部署级配置的取值优先级
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
MULTICA_SERVER_URL / MULTICA_APP_URL 环境变量(安装时)
|
|
12
|
+
> manifest.json 顶层 server_url / app_url(运维改下载源即可,无需重发 npm 包)
|
|
13
|
+
> 包内 lib/dist-config.json(发布时按 profile 固化)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
二进制下载源基址同样三级:`MULTICA_DOWNLOAD_BASE` 环境变量 > manifest 对应环境的 `lib/dist-config.json` 的 `download_base` > 包内置默认。
|
|
17
|
+
|
|
18
|
+
用户 `~/.multica/config.json` 中已有的值**一律不覆盖**,仅提示。
|
|
19
|
+
|
|
20
|
+
## 适用人群与前置
|
|
21
|
+
|
|
22
|
+
- 内网办公电脑/服务器上的开发者,机器可访问下载源(可覆盖)
|
|
23
|
+
- 已安装 Node.js ≥ 18 与 npm
|
|
24
|
+
- 支持平台:darwin / linux / windows × amd64 / arm64
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 全局安装(推荐:npm 全局 bin 目录已在 PATH)
|
|
30
|
+
npm i -g @asiainfo-sdd/multica-cli
|
|
31
|
+
|
|
32
|
+
# 或项目内安装(用 npx 调用)
|
|
33
|
+
npm i @asiainfo-sdd/multica-cli
|
|
34
|
+
npx multica version
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
安装完成后按提示自行完成剩余步骤(若下载源已带 server_url 则只需两步):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
multica config set server_url https://<服务器地址> # 仅当安装时未自动写入
|
|
41
|
+
multica login --token mul_<个人令牌> # 令牌在 Web 端“设置→令牌”生成
|
|
42
|
+
multica daemon start # 需本机已装至少一个 agent CLI(claude/codex/opencode…)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
日常管理:`multica daemon status` / `multica daemon logs` / `multica daemon stop`。
|
|
46
|
+
|
|
47
|
+
## 环境变量
|
|
48
|
+
|
|
49
|
+
| 变量 | 作用 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `MULTICA_DOWNLOAD_BASE` | 覆盖二进制下载源基址(源根需有 `manifest.json` 与 `v<版本>/` 目录) |
|
|
52
|
+
| `MULTICA_SERVER_URL` / `MULTICA_APP_URL` | 安装时写入 `~/.multica/config.json` 的部署级配置(优先级高于 manifest 与包内置;不覆盖已有值) |
|
|
53
|
+
| `MULTICA_SKIP_BINARY_INSTALL=1` | 跳过 postinstall 下载(`--ignore-scripts` 环境或自备二进制时用) |
|
|
54
|
+
| `http_proxy` / `https_proxy` / `NO_PROXY` | 走内网代理时识别(Node 自带 fetch 不认这些变量,本包已自实现) |
|
|
55
|
+
|
|
56
|
+
## 故障恢复
|
|
57
|
+
|
|
58
|
+
| 症状 | 处理 |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| 安装时看不到“上手”提示 | npm ≥ 7 默认隐藏 postinstall 输出(脚本失败时才显示);加 `--foreground-scripts` 参数即可看到,或安装后执行 `multica version` 确认可用 |
|
|
61
|
+
| 执行 `multica` 提示二进制未下载 | `npm rebuild @asiainfo-sdd/multica-cli`(常见于 `--ignore-scripts` 安装或当时下载源不可达) |
|
|
62
|
+
| 提示“multica 不是命令” | 重新打开终端;或用 `npx multica ...` |
|
|
63
|
+
| 升级时提示文件被占用 | daemon 正在运行:先 `multica daemon stop`,再 `npm i -g @asiainfo-sdd/multica-cli@<版本>` |
|
|
64
|
+
| 下载源不可达 | 确认 `MULTICA_DOWNLOAD_BASE`;检查代理变量 |
|
|
65
|
+
|
|
66
|
+
## 升级
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm i -g @asiainfo-sdd/multica-cli@<新版本>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
npm 包版本与二进制版本一一对应;postinstall 检测到版本变化会重新下载。
|
|
73
|
+
daemon 的 auto-reload 默认开启:二进制替换后运行中的 daemon 会自动重启到新版本。
|
|
74
|
+
|
|
75
|
+
## 版本发布(维护者)
|
|
76
|
+
|
|
77
|
+
本包位于 multica 仓 `apps/multica-cli-installer/`(与 `server/` 同仓管理,随主仓 git 提交发布)。
|
|
78
|
+
同一份源码按 profile 发布两个 npm 包(差异仅包名与内置部署级配置),发布流程:
|
|
79
|
+
|
|
80
|
+
1. 本仓 `server/extensions/agentfoundry/build/build.sh cli-all <版本>`:
|
|
81
|
+
6 平台 CLI 产物 + `manifest.json` 落 `server/extensions/agentfoundry/dist/cli/`
|
|
82
|
+
2. 如需随包分发部署级配置,在 `manifest.json` 顶层加 `server_url` / `app_url`
|
|
83
|
+
3. 将 `dist/cli/` 整目录同步到目标环境下载源的 HTTP 静态根(MinIO `daemon-download/` 前缀)
|
|
84
|
+
4. 在 `apps/multica-cli-installer/` 下按 profile 发布:
|
|
85
|
+
- 生产包:`node scripts/publish.mjs prod [版本]`
|
|
86
|
+
- 测试包:`node scripts/publish.mjs test`(连接测试环境,配置见 `publish/profiles.json`)
|
|
87
|
+
- 加 `--dry-run` 只打 tarball 检查,不实际发布
|
|
88
|
+
5. 版本号在源 `package.json` 上推进(`lib/dist-config.json` 保持生产默认值;测试包的差异由 profile 注入)
|
|
89
|
+
|
|
90
|
+
> 两包 bin 命令名均为 `multica`,同一台机器互斥安装;切换环境用
|
|
91
|
+
> `npm rm -g <A> && npm i -g <B>`,安装时会自动覆盖式更新 `~/.multica/config.json`
|
|
92
|
+
> 的 server_url/app_url 并提示(token 需重新登录生成)。
|
|
93
|
+
|
|
94
|
+
## FAQ
|
|
95
|
+
|
|
96
|
+
**Q: 为什么不把二进制打进 npm 包?**
|
|
97
|
+
每个平台 17-68MB,6 平台约 200MB;HTTP 运行时下载让 npm 包保持 KB 级,且安装哪台机器才拉哪个平台。
|
|
98
|
+
|
|
99
|
+
**Q: postinstall 下载失败了怎么办?**
|
|
100
|
+
安装不会失败到阻断——失败时打印修复指引,之后 `npm rebuild @asiainfo-sdd/multica-cli` 重试。
|
|
101
|
+
|
|
102
|
+
**Q: daemon 会自动升级吗?**
|
|
103
|
+
self-host 场景下 daemon 的 auto-update 默认关闭;升级由你通过 npm 主动控制。
|
package/bin/multica.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// bin wrapper:把参数原样转给 vendor/ 下的平台二进制。
|
|
5
|
+
// 对外唯一命令名是 multica(aido 入口已移除)。
|
|
6
|
+
// 二进制的子命令(daemon/login/config/...)与参数原样透传。
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { detectPlatform, vendorBinaryPath } = require('../lib/platform');
|
|
12
|
+
const pkg = require('../package.json');
|
|
13
|
+
|
|
14
|
+
const plat = detectPlatform();
|
|
15
|
+
const binary =
|
|
16
|
+
plat && vendorBinaryPath(path.join(__dirname, '..', 'vendor'), plat);
|
|
17
|
+
|
|
18
|
+
if (!binary || !fs.existsSync(binary)) {
|
|
19
|
+
console.error('✗ multica 二进制尚未下载。');
|
|
20
|
+
console.error(` 修复方式:npm rebuild ${pkg.name}`);
|
|
21
|
+
console.error(' (或设置 MULTICA_SKIP_BINARY_INSTALL=1 显式跳过后自备二进制)');
|
|
22
|
+
process.exit(127);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const child = spawn(binary, process.argv.slice(2), {
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Windows 上 Ctrl-C 走 SIGINT/Break;转发常见终止信号。
|
|
31
|
+
['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'].forEach((sig) => {
|
|
32
|
+
process.on(sig, () => child.kill(sig));
|
|
33
|
+
});
|
|
34
|
+
// Windows spawn .exe 时 kill('SIGTERM') 可能不生效,兜底强杀。
|
|
35
|
+
process.on('exit', () => {
|
|
36
|
+
if (!child.killed) child.kill();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
child.on('error', (err) => {
|
|
40
|
+
console.error(`✗ 无法执行 multica 二进制:${err.message}`);
|
|
41
|
+
process.exit(126);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.on('close', (code, signal) => {
|
|
45
|
+
if (signal) {
|
|
46
|
+
process.exit(128);
|
|
47
|
+
}
|
|
48
|
+
process.exit(code ?? 1);
|
|
49
|
+
});
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 零依赖 HTTP 客户端:Node 自带 fetch(undici)不认 http_proxy/https_proxy
|
|
4
|
+
// 环境变量,内网代理场景必须自己处理。支持:
|
|
5
|
+
// - 直连 http/https
|
|
6
|
+
// - http_proxy / https_proxy / HTTP_PROXY / HTTPS_PROXY
|
|
7
|
+
// - NO_PROXY(逗号分隔主机名,支持后缀匹配)
|
|
8
|
+
// - https over proxy:CONNECT 隧道
|
|
9
|
+
// 流式响应,供下载侧边收边算 sha256。
|
|
10
|
+
|
|
11
|
+
const http = require('http');
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const { URL } = require('url');
|
|
14
|
+
|
|
15
|
+
const KEEP_ALIVE_AGENT_OPTIONS = { keepAlive: false };
|
|
16
|
+
|
|
17
|
+
function proxyFromEnv(targetURL) {
|
|
18
|
+
const url = new URL(targetURL);
|
|
19
|
+
if (matchesNoProxy(url.hostname)) return null;
|
|
20
|
+
const proto = url.protocol === 'https:' ? 'https' : 'http';
|
|
21
|
+
const raw =
|
|
22
|
+
process.env[`${proto}_proxy`] || process.env[`${proto}_PROXY`] || '';
|
|
23
|
+
if (!raw) return null;
|
|
24
|
+
try {
|
|
25
|
+
const proxy = new URL(raw);
|
|
26
|
+
if (!/^https?:$/.test(proxy.protocol)) return null;
|
|
27
|
+
return proxy;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesNoProxy(hostname) {
|
|
34
|
+
const list = (process.env.NO_PROXY || process.env.NO_PROXY === '' ? process.env.NO_PROXY : process.env.no_proxy) || '';
|
|
35
|
+
if (!list) return false;
|
|
36
|
+
return list
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((s) => s.trim().toLowerCase())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.some((entry) => {
|
|
41
|
+
if (entry === '*') return true;
|
|
42
|
+
if (entry.startsWith('.')) return hostname.endsWith(entry);
|
|
43
|
+
return hostname === entry || hostname.endsWith(`.${entry}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// http 目标 + http 代理:向代理发绝对 URI 的 GET。
|
|
48
|
+
// https 目标 + 任意代理:先 CONNECT 隧道再在隧道上发 GET。
|
|
49
|
+
function request(targetURL, { headers = {}, timeoutMs = 120000 } = {}) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(targetURL);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
reject(new Error(`invalid URL: ${targetURL}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const proxy = proxyFromEnv(targetURL);
|
|
59
|
+
|
|
60
|
+
const onConnectError = (err) =>
|
|
61
|
+
reject(new Error(`proxy connect failed: ${err.message}`));
|
|
62
|
+
|
|
63
|
+
if (url.protocol === 'https:' && proxy) {
|
|
64
|
+
const proxyPort = Number(proxy.port) || (proxy.protocol === 'https:' ? 443 : 80);
|
|
65
|
+
const connectReq = http.request({
|
|
66
|
+
host: proxy.hostname,
|
|
67
|
+
port: proxyPort,
|
|
68
|
+
method: 'CONNECT',
|
|
69
|
+
path: `${url.hostname}:${url.port || 443}`,
|
|
70
|
+
headers: { Host: `${url.hostname}:${url.port || 443}` },
|
|
71
|
+
timeout: timeoutMs,
|
|
72
|
+
});
|
|
73
|
+
connectReq.on('connect', (res, socket) => {
|
|
74
|
+
if (res.statusCode !== 200) {
|
|
75
|
+
socket.destroy();
|
|
76
|
+
reject(new Error(`proxy CONNECT failed: ${res.statusCode}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const agent = new https.Agent({ ...KEEP_ALIVE_AGENT_OPTIONS, socket });
|
|
80
|
+
const req = https.get(
|
|
81
|
+
{
|
|
82
|
+
host: url.hostname,
|
|
83
|
+
port: url.port || 443,
|
|
84
|
+
path: url.pathname + url.search,
|
|
85
|
+
headers,
|
|
86
|
+
agent,
|
|
87
|
+
timeout: timeoutMs,
|
|
88
|
+
},
|
|
89
|
+
resolve,
|
|
90
|
+
);
|
|
91
|
+
req.on('timeout', () => {
|
|
92
|
+
req.destroy(new Error(`request timed out after ${timeoutMs}ms`));
|
|
93
|
+
});
|
|
94
|
+
req.on('error', reject);
|
|
95
|
+
});
|
|
96
|
+
connectReq.on('error', onConnectError);
|
|
97
|
+
connectReq.on('timeout', () => {
|
|
98
|
+
connectReq.destroy(new Error(`proxy connect timed out after ${timeoutMs}ms`));
|
|
99
|
+
});
|
|
100
|
+
connectReq.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isHttps = url.protocol === 'https:';
|
|
105
|
+
const mod = isHttps ? https : http;
|
|
106
|
+
const options = {
|
|
107
|
+
host: proxy ? proxy.hostname : url.hostname,
|
|
108
|
+
port: proxy
|
|
109
|
+
? Number(proxy.port) || (proxy.protocol === 'https:' ? 443 : 80)
|
|
110
|
+
: Number(url.port) || (isHttps ? 443 : 80),
|
|
111
|
+
method: 'GET',
|
|
112
|
+
path: proxy ? targetURL : url.pathname + url.search,
|
|
113
|
+
headers,
|
|
114
|
+
timeout: timeoutMs,
|
|
115
|
+
};
|
|
116
|
+
// https 代理入口(少见于内网)不支持时直接报错,不做二次降级。
|
|
117
|
+
if (proxy && proxy.protocol === 'https:') {
|
|
118
|
+
reject(new Error('https proxy entry is not supported'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const req = mod.get(options, resolve);
|
|
122
|
+
req.on('timeout', () => {
|
|
123
|
+
req.destroy(new Error(`request timed out after ${timeoutMs}ms`));
|
|
124
|
+
});
|
|
125
|
+
req.on('error', reject);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkStatus(res, what) {
|
|
130
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
131
|
+
res.resume();
|
|
132
|
+
throw new Error(`${what} failed: HTTP ${res.statusCode} ${res.statusMessage || ''}`.trim());
|
|
133
|
+
}
|
|
134
|
+
return res;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function fetchText(url, { timeoutMs = 10000 } = {}) {
|
|
138
|
+
const res = checkStatus(await request(url, { timeoutMs }), `fetch ${url}`);
|
|
139
|
+
const chunks = [];
|
|
140
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
141
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 流式下载到可写流,返回 { statusCode 后已校验的响应流 }。
|
|
145
|
+
// onProgress(bytesDone, totalOrNull) 仅在有内容时回调。
|
|
146
|
+
function downloadToStream(url, dest, { timeoutMs = 300000, onProgress } = {}) {
|
|
147
|
+
return new Promise(async (resolve, reject) => {
|
|
148
|
+
let res;
|
|
149
|
+
try {
|
|
150
|
+
res = await request(url, { timeoutMs });
|
|
151
|
+
checkStatus(res, `download ${url}`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
reject(err);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const total = Number(res.headers['content-length']) || null;
|
|
157
|
+
let done = 0;
|
|
158
|
+
let lastTick = 0;
|
|
159
|
+
res.on('data', (chunk) => {
|
|
160
|
+
done += chunk.length;
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
if (onProgress && (now - lastTick > 200 || done === total)) {
|
|
163
|
+
lastTick = now;
|
|
164
|
+
onProgress(done, total);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
res.on('error', reject);
|
|
168
|
+
dest.on('error', reject);
|
|
169
|
+
res.pipe(dest);
|
|
170
|
+
dest.on('finish', () => resolve());
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { request, fetchText, downloadToStream };
|
package/lib/install.js
ADDED
|
@@ -0,0 +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
|
+
// 下载源兜底基址(生产)。实际取值: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
|
+
});
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// platform/arch → 下载资产维度。产物命名与 multica 仓
|
|
4
|
+
// server/extensions/agentfoundry/build/build.sh 的 cli-all 模式对齐:
|
|
5
|
+
// multica-af-<os>-<arch>[.exe]
|
|
6
|
+
|
|
7
|
+
const OS_MAP = {
|
|
8
|
+
darwin: 'darwin',
|
|
9
|
+
linux: 'linux',
|
|
10
|
+
win32: 'windows',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ARCH_MAP = {
|
|
14
|
+
x64: 'amd64',
|
|
15
|
+
arm64: 'arm64',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 返回 null 表示当前平台不在 6 平台支持矩阵内。
|
|
19
|
+
function detectPlatform(platform = process.platform, arch = process.arch) {
|
|
20
|
+
const os = OS_MAP[platform];
|
|
21
|
+
const mappedArch = ARCH_MAP[arch];
|
|
22
|
+
if (!os || !mappedArch) return null;
|
|
23
|
+
return { os, arch: mappedArch, ext: platform === 'win32' ? '.exe' : '' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assetName({ os, arch, ext }) {
|
|
27
|
+
return `multica-af-${os}-${arch}${ext}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// manifest.json 里的平台键。
|
|
31
|
+
function manifestKey({ os, arch }) {
|
|
32
|
+
return `${os}-${arch}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function vendorBinaryPath(vendorDir, plat) {
|
|
36
|
+
const path = require('path');
|
|
37
|
+
return path.join(vendorDir, assetName(plat));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
detectPlatform,
|
|
42
|
+
assetName,
|
|
43
|
+
manifestKey,
|
|
44
|
+
vendorBinaryPath,
|
|
45
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 安装时把部署级配置(server_url/app_url)写入 ~/.multica/config.json,
|
|
4
|
+
// 免去用户手工 multica config set。规则:
|
|
5
|
+
// - 取值优先级: MULTICA_SERVER_URL/MULTICA_APP_URL env > manifest 顶层
|
|
6
|
+
// server_url/app_url(运维可不重发 npm 包直接改) > lib/dist-config.json
|
|
7
|
+
// - server_url/app_url 两个部署级 URL【覆盖式】更新: 与本次安装源不一致时
|
|
8
|
+
// 直接覆盖并打印 新值 ← 旧值(升级环境后旧地址上的 token 验证/daemon 连接
|
|
9
|
+
// 必然失败,覆盖是唯一正确动作;自定义地址用户可从日志看到旧值并改回)
|
|
10
|
+
// - 其余字段(token/workspace_id/各超时/backends 等)一律不动
|
|
11
|
+
// - 目标文件是 multica CLI 的标准配置(daemon 只认这里的 token),
|
|
12
|
+
// 多 profile 场景(--profile)不在此处理,留给用户手动 config set
|
|
13
|
+
// token 属于个人凭据,始终由用户自行 multica login --token 写入。
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const CONFIG_KEYS = ['server_url', 'app_url'];
|
|
20
|
+
|
|
21
|
+
function multicaConfigPath() {
|
|
22
|
+
return path.join(os.homedir(), '.multica', 'config.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readUserConfig() {
|
|
26
|
+
const file = multicaConfigPath();
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err.code === 'ENOENT') return { config: {}, existed: false };
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
const config = JSON.parse(raw); // 解析失败向上抛,由调用方决定跳过
|
|
35
|
+
return { config, existed: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// env > manifest > dist-config;返回只含非空键的 {server_url?, app_url?}。
|
|
39
|
+
function resolveDesired(manifest) {
|
|
40
|
+
const distConfig = require('./dist-config.json');
|
|
41
|
+
const desired = {};
|
|
42
|
+
const fromEnv = {
|
|
43
|
+
server_url: process.env.MULTICA_SERVER_URL,
|
|
44
|
+
app_url: process.env.MULTICA_APP_URL,
|
|
45
|
+
};
|
|
46
|
+
for (const key of CONFIG_KEYS) {
|
|
47
|
+
const value =
|
|
48
|
+
(fromEnv[key] && fromEnv[key].trim()) ||
|
|
49
|
+
(manifest && typeof manifest[key] === 'string' && manifest[key].trim()) ||
|
|
50
|
+
(typeof distConfig[key] === 'string' && distConfig[key].trim()) ||
|
|
51
|
+
'';
|
|
52
|
+
if (value) desired[key] = value;
|
|
53
|
+
}
|
|
54
|
+
return desired;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 返回 {written: [{key, value}], updated: [{key, from, to}]} 供上层打印;
|
|
58
|
+
// 无可写内容或文件损坏时返回 null(不产生任何变更)。
|
|
59
|
+
// written = 原来为空、本次填入;updated = 原有不同值、本次覆盖。
|
|
60
|
+
function applyServerConfig(manifest) {
|
|
61
|
+
const desired = resolveDesired(manifest);
|
|
62
|
+
if (!desired.server_url && !desired.app_url) return null;
|
|
63
|
+
|
|
64
|
+
let userConfig;
|
|
65
|
+
let existed;
|
|
66
|
+
try {
|
|
67
|
+
({ config: userConfig, existed } = readUserConfig());
|
|
68
|
+
} catch {
|
|
69
|
+
logWarn(`已存在 ${multicaConfigPath()} 但无法解析,已跳过配置写入——请手工 multica config set server_url`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const written = [];
|
|
74
|
+
const updated = [];
|
|
75
|
+
let changed = false;
|
|
76
|
+
for (const key of CONFIG_KEYS) {
|
|
77
|
+
if (!desired[key]) continue;
|
|
78
|
+
if (!userConfig[key]) {
|
|
79
|
+
userConfig[key] = desired[key];
|
|
80
|
+
written.push({ key, value: desired[key] });
|
|
81
|
+
changed = true;
|
|
82
|
+
} else if (userConfig[key] !== desired[key]) {
|
|
83
|
+
updated.push({ key, from: userConfig[key], to: desired[key] });
|
|
84
|
+
userConfig[key] = desired[key];
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!changed) return { written, updated };
|
|
90
|
+
|
|
91
|
+
const file = multicaConfigPath();
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
93
|
+
// 保留用户已有其余字段,仅更新部署级键;不写 token。
|
|
94
|
+
fs.writeFileSync(file, JSON.stringify(userConfig, null, 2) + '\n');
|
|
95
|
+
return { written, updated };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function logWarn(msg) {
|
|
99
|
+
process.stdout.write(`⚠ ${msg}\n`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { applyServerConfig, multicaConfigPath, resolveDesired };
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asiainfo-sdd/multica-cli-test",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "数字工厂 daemon 客户端安装帮助包(测试环境):连接测试环境服务器与测试环境二进制下载源",
|
|
5
|
+
"bin": {
|
|
6
|
+
"multica": "bin/multica.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"license": "UNLICENSED",
|
|
17
|
+
"multica": {
|
|
18
|
+
"configKeysWrittenAtInstall": [
|
|
19
|
+
"server_url",
|
|
20
|
+
"app_url"
|
|
21
|
+
],
|
|
22
|
+
"notes": "安装时只填空写入部署级配置(env MULTICA_SERVER_URL/MULTICA_APP_URL > manifest 顶层 > lib/dist-config.json);不写 token、不覆盖已有值"
|
|
23
|
+
}
|
|
24
|
+
}
|