zcode-idea 0.0.0-stage → 0.0.1
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 +295 -2
- package/bin/zcode-idea.mjs +463 -0
- package/lib/download.mjs +294 -0
- package/lib/errors.mjs +17 -0
- package/lib/installed.mjs +86 -0
- package/lib/launch.mjs +214 -0
- package/lib/log.mjs +36 -0
- package/lib/manifest.mjs +301 -0
- package/lib/plugin-signing-public.pem +3 -0
- package/lib/processes.mjs +194 -0
- package/lib/products.mjs +352 -0
- package/lib/prompt.mjs +37 -0
- package/lib/remote.mjs +356 -0
- package/lib/targets.mjs +415 -0
- package/lib/unzip.mjs +157 -0
- package/package.json +35 -4
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* zcode-idea:从七牛 CDN 获取插件包并安装到本机 JetBrains IDE。
|
|
4
|
+
*
|
|
5
|
+
* 主流程:
|
|
6
|
+
* 1. 获取插件来源:远端 manifest(Ed25519 验签)或 --zip 本地包
|
|
7
|
+
* 2. 发现 IDE(配置目录优先;安装目录需通过写探针)
|
|
8
|
+
* 3. 选择目标(--all / --target 可跳过)
|
|
9
|
+
* 4. 对比已安装版本(可更新 / 已是最新 / 拒绝降级;--force 覆盖)
|
|
10
|
+
* 5. 关闭所选 IDE(仅需要安装的目标)
|
|
11
|
+
* 6. 下载(缓存 + sha256 校验)→ 删旧目录 → 解压
|
|
12
|
+
* 7. 汇总并启动 IDE(--no-launch 可跳过)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import fsp from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import cac from "cac";
|
|
19
|
+
import {
|
|
20
|
+
ensurePluginArchive,
|
|
21
|
+
getCachedArchivePath,
|
|
22
|
+
isCachePresent,
|
|
23
|
+
prunePluginCache,
|
|
24
|
+
} from "../lib/download.mjs";
|
|
25
|
+
import { InstallError, UsageError } from "../lib/errors.mjs";
|
|
26
|
+
import { readInstalledPlugin } from "../lib/installed.mjs";
|
|
27
|
+
import { findLaunchCommand, launchDetached } from "../lib/launch.mjs";
|
|
28
|
+
import { error, formatBytes, info, isInteractive, warn } from "../lib/log.mjs";
|
|
29
|
+
import { validatePluginZip } from "../lib/manifest.mjs";
|
|
30
|
+
import { closeSelectedProcesses } from "../lib/processes.mjs";
|
|
31
|
+
import { confirmClose, selectTargets } from "../lib/prompt.mjs";
|
|
32
|
+
import {
|
|
33
|
+
comparePluginVersions,
|
|
34
|
+
fetchManifestBundle,
|
|
35
|
+
resolvePluginSource,
|
|
36
|
+
} from "../lib/remote.mjs";
|
|
37
|
+
import { discoverTargetsDetailed, formatTarget, makeExplicitTarget, parseTargetArgs } from "../lib/targets.mjs";
|
|
38
|
+
import { PLUGIN_DIR_NAME, extractPluginZip } from "../lib/unzip.mjs";
|
|
39
|
+
|
|
40
|
+
const pkg = JSON.parse(await fsp.readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
41
|
+
|
|
42
|
+
function buildCli() {
|
|
43
|
+
const cli = cac("zcode-idea");
|
|
44
|
+
cli.usage(
|
|
45
|
+
"[--all] [--dry-run] [--check] [--yes] [--force] [--no-launch] " +
|
|
46
|
+
"[--source <url>] [--zip <path>] [--allow-http] [--target <dir>] [--timeout <秒>] " +
|
|
47
|
+
"[--clean-cache]",
|
|
48
|
+
);
|
|
49
|
+
cli
|
|
50
|
+
.option("--all", "安装到全部发现的 IDE(跳过交互选择)")
|
|
51
|
+
.option("--dry-run", "获取 manifest 并打印版本对比与计划;不下载、不关 IDE、不解压、不启动")
|
|
52
|
+
.option("--check", "只打印版本对比表后退出(不下载、不关 IDE、不安装)")
|
|
53
|
+
.option("--yes", "跳过关闭 IDE 前的确认")
|
|
54
|
+
.option("--force", "已是最新或远端版本更低时也强制安装")
|
|
55
|
+
.option("--no-launch", "安装完成后不启动 IDE(默认会启动)")
|
|
56
|
+
.option("--source <url>", "插件分发根 URL(也可用 ZCODE_IDE_PLUGIN_BASE_URL)")
|
|
57
|
+
.option("--zip <path>", "离线:直接使用本地插件 zip,跳过远端 manifest 与下载")
|
|
58
|
+
.option("--allow-http", "允许非默认的 HTTP 插件源(仅测试域名联调)")
|
|
59
|
+
.option("--target <dir>", "直接指定 plugins 目录(可重复或逗号分隔),跳过发现与选择")
|
|
60
|
+
.option("--timeout <秒>", "等待 IDE 退出的秒数", { default: 30 })
|
|
61
|
+
.option("--clean-cache", "安装结束后清理其它版本的插件包缓存(保留本次使用的版本)");
|
|
62
|
+
|
|
63
|
+
// cac 会为 --no-* 自动补 default: true,帮助里显示成“(default: true)”容易被误解。
|
|
64
|
+
// 这里只清掉帮助展示用的默认值:不传 --no-launch 时 launch 仍是 undefined(会启动),
|
|
65
|
+
// 传了则由 mri 置为 false,语义不变。
|
|
66
|
+
const noLaunchOption = cli.globalCommand?.options?.find(
|
|
67
|
+
(option) => option.rawName === "--no-launch",
|
|
68
|
+
);
|
|
69
|
+
if (noLaunchOption?.config) noLaunchOption.config.default = undefined;
|
|
70
|
+
|
|
71
|
+
cli.help();
|
|
72
|
+
cli.version(pkg.version);
|
|
73
|
+
cli.example("npx zcode-idea");
|
|
74
|
+
cli.example("npx zcode-idea --check --all");
|
|
75
|
+
cli.example("npx zcode-idea --all --yes");
|
|
76
|
+
cli.example("npx zcode-idea --zip ./build/distributions/zcode-ide-0.1.0.zip --all --no-launch");
|
|
77
|
+
return cli;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function targetLabel(target) {
|
|
81
|
+
return `${target.productName}${target.version ? ` ${target.version}` : ""}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function statusLabel(row, force) {
|
|
85
|
+
if (row.status === "missing") return force ? "强制安装" : "将安装";
|
|
86
|
+
if (row.status === "update") return force ? "强制更新" : "可更新";
|
|
87
|
+
if (row.status === "latest") return force ? "强制重装" : "已是最新(跳过)";
|
|
88
|
+
return force ? "强制降级安装" : "跳过(远端版本更低)";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function main() {
|
|
92
|
+
const cli = buildCli();
|
|
93
|
+
cli.parse(process.argv, { run: false });
|
|
94
|
+
if (cli.options.help || cli.options.version) return 0;
|
|
95
|
+
|
|
96
|
+
const options = cli.options;
|
|
97
|
+
const dryRun = Boolean(options.dryRun);
|
|
98
|
+
const checkOnly = Boolean(options.check);
|
|
99
|
+
const force = Boolean(options.force);
|
|
100
|
+
const allowHttp = Boolean(options.allowHttp);
|
|
101
|
+
const assumeYes = Boolean(options.yes);
|
|
102
|
+
const installAll = Boolean(options.all);
|
|
103
|
+
const noLaunch = options.launch === false;
|
|
104
|
+
const cleanCache = Boolean(options.cleanCache);
|
|
105
|
+
const timeoutSec = Number(options.timeout ?? 30);
|
|
106
|
+
const interactive = isInteractive();
|
|
107
|
+
|
|
108
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec <= 0 || timeoutSec > 600) {
|
|
109
|
+
throw new UsageError("--timeout 必须是 1–600 之间的秒数。");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const explicitPaths = parseTargetArgs(options.target);
|
|
113
|
+
if (!interactive && !installAll && explicitPaths.length === 0) {
|
|
114
|
+
throw new UsageError(
|
|
115
|
+
"非交互终端:请用 --all 安装到全部发现的 IDE," +
|
|
116
|
+
"或用 --target <plugins 目录> 指定目标。",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
for (const targetPath of explicitPaths) {
|
|
120
|
+
if (fs.existsSync(targetPath) && !fs.statSync(targetPath).isDirectory()) {
|
|
121
|
+
throw new UsageError(`--target 指向的不是目录:${targetPath}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const localZipArg =
|
|
126
|
+
typeof options.zip === "string" && options.zip.trim() ? options.zip.trim() : null;
|
|
127
|
+
if (localZipArg && (options.source || process.env.ZCODE_IDE_PLUGIN_BASE_URL)) {
|
|
128
|
+
warn("--zip 模式会忽略 --source / ZCODE_IDE_PLUGIN_BASE_URL。");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
info(
|
|
132
|
+
`zcode-idea ${pkg.version} 开始${dryRun ? "(dry-run 模式)" : ""}` +
|
|
133
|
+
`${checkOnly ? "(仅检查模式)" : ""}。`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/* ------------------------------ 1. 插件来源 ------------------------------ */
|
|
137
|
+
let plugin;
|
|
138
|
+
if (localZipArg) {
|
|
139
|
+
info("第 1 步:使用本地插件包(--zip),跳过远端 manifest 与下载。");
|
|
140
|
+
const localZipPath = path.resolve(localZipArg);
|
|
141
|
+
if (!fs.existsSync(localZipPath) || !fs.statSync(localZipPath).isFile()) {
|
|
142
|
+
throw new InstallError(`--zip 指定的插件包不存在或不是文件:${localZipPath}`);
|
|
143
|
+
}
|
|
144
|
+
const zipInfo = await validatePluginZip(localZipPath);
|
|
145
|
+
plugin = { mode: "local", pluginVersion: zipInfo.pluginVersion, zipInfo, zipPath: localZipPath };
|
|
146
|
+
info(
|
|
147
|
+
`本地插件包校验通过:${localZipPath}(插件版本 ${zipInfo.pluginVersion},` +
|
|
148
|
+
`${formatBytes(zipInfo.size)},sha256 ${zipInfo.sha256.slice(0, 12)}…)`,
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
const source = resolvePluginSource({
|
|
152
|
+
source: options.source,
|
|
153
|
+
env: process.env,
|
|
154
|
+
allowHttp,
|
|
155
|
+
});
|
|
156
|
+
if (source.isDefault) {
|
|
157
|
+
warn("正在使用七牛测试域名(HTTP),生产环境请用 --source 指定 HTTPS 域名。");
|
|
158
|
+
} else if (source.isHttp) {
|
|
159
|
+
warn(`使用 HTTP 插件源(仅限联调):${source.baseUrl}`);
|
|
160
|
+
}
|
|
161
|
+
info(`第 1 步:获取并校验远端 manifest:${source.baseUrl}/manifest.json`);
|
|
162
|
+
const bundle = await fetchManifestBundle({ baseUrl: source.baseUrl, retries: 3 });
|
|
163
|
+
plugin = {
|
|
164
|
+
mode: "remote",
|
|
165
|
+
pluginVersion: bundle.manifest.pluginVersion,
|
|
166
|
+
sha256: bundle.manifest.sha256,
|
|
167
|
+
size: bundle.manifest.size,
|
|
168
|
+
archiveUrl: bundle.archiveUrl,
|
|
169
|
+
manifestUrl: bundle.manifestUrl,
|
|
170
|
+
cachePath: getCachedArchivePath(bundle.manifest.sha256),
|
|
171
|
+
zipInfo: null,
|
|
172
|
+
zipPath: null,
|
|
173
|
+
};
|
|
174
|
+
info(
|
|
175
|
+
`manifest 验签通过:插件版本 ${plugin.pluginVersion},${formatBytes(plugin.size)},` +
|
|
176
|
+
`sha256 ${plugin.sha256.slice(0, 12)}…`,
|
|
177
|
+
);
|
|
178
|
+
info(`插件包地址:${plugin.archiveUrl}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --clean-cache 只保留本次使用的版本;--zip 本地包按自身 sha256 保留同内容缓存。
|
|
182
|
+
const cacheKeep =
|
|
183
|
+
plugin.mode === "remote" ? [plugin.sha256] : [plugin.zipInfo.sha256];
|
|
184
|
+
const cacheKeepNote =
|
|
185
|
+
plugin.mode === "remote" ? ",保留本次使用的版本" : ",保留本次本地包对应的缓存";
|
|
186
|
+
|
|
187
|
+
/* ------------------------------ 2. 发现 ------------------------------ */
|
|
188
|
+
let targets;
|
|
189
|
+
if (explicitPaths.length > 0) {
|
|
190
|
+
info(`第 2 步:使用 --target 指定的 ${explicitPaths.length} 个目标(跳过发现)。`);
|
|
191
|
+
targets = explicitPaths.map(makeExplicitTarget);
|
|
192
|
+
for (const target of targets) info(` - ${formatTarget(target)}`);
|
|
193
|
+
} else {
|
|
194
|
+
info("第 2 步:发现本机 JetBrains IDE…");
|
|
195
|
+
const discovered = discoverTargetsDetailed();
|
|
196
|
+
if (discovered.skipped.length > 0) {
|
|
197
|
+
warn(
|
|
198
|
+
`已跳过 ${discovered.skipped.length} 个不可写的 IDE 安装目录(需要管理员权限)。`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
targets = discovered.targets;
|
|
202
|
+
if (targets.length === 0) {
|
|
203
|
+
throw new InstallError(
|
|
204
|
+
"没有发现任何可用的 JetBrains IDE。可以用 --target <plugins 目录> 直接指定安装位置,例如:" +
|
|
205
|
+
"C:\\Users\\<你>\\AppData\\Roaming\\JetBrains\\IntelliJIdea2026.2\\plugins",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
info(`发现 ${targets.length} 个可用目标:`);
|
|
209
|
+
targets.forEach((target, index) => info(` ${index + 1}) ${formatTarget(target)}`));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ------------------------------ 3. 选择 ------------------------------ */
|
|
213
|
+
if (explicitPaths.length > 0) {
|
|
214
|
+
info(`第 3 步:已由 --target 指定 ${targets.length} 个目标,跳过选择。`);
|
|
215
|
+
} else if (installAll) {
|
|
216
|
+
info(`第 3 步:--all,处理全部 ${targets.length} 个目标。`);
|
|
217
|
+
} else if (targets.length === 1) {
|
|
218
|
+
info(`第 3 步:只有一个目标,自动选择:${formatTarget(targets[0])}`);
|
|
219
|
+
} else {
|
|
220
|
+
info("第 3 步:请选择要安装的 IDE。");
|
|
221
|
+
targets = await selectTargets(targets);
|
|
222
|
+
info(`已选择 ${targets.length} 个目标。`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ------------------------------ 4. 版本对比 ------------------------------ */
|
|
226
|
+
info("第 4 步:对比已安装版本…");
|
|
227
|
+
const rows = [];
|
|
228
|
+
for (const target of targets) {
|
|
229
|
+
const installed = await readInstalledPlugin(target.pluginsDir);
|
|
230
|
+
if (installed.installed && !installed.version) {
|
|
231
|
+
warn(
|
|
232
|
+
`${targetLabel(target)}:存在 zcode-ide 但无法读取版本` +
|
|
233
|
+
`(${installed.error ?? "未知原因"}),按未安装处理。`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
let status;
|
|
237
|
+
if (!installed.version) status = "missing";
|
|
238
|
+
else {
|
|
239
|
+
const comparison = comparePluginVersions(installed.version, plugin.pluginVersion);
|
|
240
|
+
status = comparison < 0 ? "update" : comparison === 0 ? "latest" : "downgrade";
|
|
241
|
+
}
|
|
242
|
+
const action =
|
|
243
|
+
force || status === "missing" || status === "update" ? "install" : "skip";
|
|
244
|
+
rows.push({ target, installedVersion: installed.version, status, action });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
info("版本对比:");
|
|
248
|
+
info(" 目标 | 已安装 | 可用 | 状态");
|
|
249
|
+
for (const row of rows) {
|
|
250
|
+
info(
|
|
251
|
+
` ${targetLabel(row.target)} | ${row.installedVersion ?? "未安装"} | ` +
|
|
252
|
+
`${plugin.pluginVersion} | ${statusLabel(row, force)}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const row of rows) {
|
|
256
|
+
if (row.status === "downgrade" && row.action === "skip") {
|
|
257
|
+
warn(
|
|
258
|
+
`跳过降级:${targetLabel(row.target)} 已安装 ${row.installedVersion},` +
|
|
259
|
+
`远端 ${plugin.pluginVersion} 更低(--force 可强制降级)。`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (checkOnly) {
|
|
265
|
+
info("--check 完成:未下载插件包、未关闭 IDE、未安装。");
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// dry-run 的清理预览是纯只读的,放在 pending 判断之前:
|
|
270
|
+
// 即使所有目标都已是最新,也能看到清理计划。
|
|
271
|
+
if (dryRun && cleanCache) {
|
|
272
|
+
try {
|
|
273
|
+
const plan = await prunePluginCache({ keep: cacheKeep, dryRun: true });
|
|
274
|
+
info(
|
|
275
|
+
`dry-run:安装后将清理 ${plan.removed.length} 个旧缓存` +
|
|
276
|
+
`(释放约 ${formatBytes(plan.freedBytes)})${cacheKeepNote}。`,
|
|
277
|
+
);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
warn(`缓存清理预览失败,已跳过:${err.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const pending = rows.filter((row) => row.action === "install");
|
|
284
|
+
if (pending.length === 0) {
|
|
285
|
+
info("没有需要安装的目标(已是最新或已跳过降级)。加 --force 可强制重装。");
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
info("安装计划:");
|
|
290
|
+
for (const row of pending) {
|
|
291
|
+
info(
|
|
292
|
+
` - ${targetLabel(row.target)} → ${path.join(row.target.pluginsDir, PLUGIN_DIR_NAME)}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
if (plugin.mode === "local") {
|
|
296
|
+
info(` 插件包:${plugin.zipPath}(本地 --zip)`);
|
|
297
|
+
} else if (isCachePresent(plugin.sha256, plugin.size)) {
|
|
298
|
+
info(` 插件包:复用本地缓存 ${plugin.cachePath}`);
|
|
299
|
+
} else {
|
|
300
|
+
info(` 插件包:下载 ${plugin.archiveUrl} → ${plugin.cachePath}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (dryRun) {
|
|
304
|
+
info("dry-run 完成:未下载插件包、未关闭 IDE、未解压、未启动。");
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* ------------------------------ 5. 关闭 IDE ------------------------------ */
|
|
309
|
+
info("第 5 步:关闭需要安装的 IDE(仅对应产品)。");
|
|
310
|
+
await closeSelectedProcesses({
|
|
311
|
+
targets: pending.map((row) => row.target),
|
|
312
|
+
timeoutSec,
|
|
313
|
+
dryRun: false,
|
|
314
|
+
assumeYes,
|
|
315
|
+
interactive,
|
|
316
|
+
confirm: confirmClose,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/* ------------------------------ 6. 准备并解压 ------------------------------ */
|
|
320
|
+
let zipInfo;
|
|
321
|
+
if (plugin.mode === "local") {
|
|
322
|
+
zipInfo = plugin.zipInfo;
|
|
323
|
+
info(`第 6 步:使用本地插件包(${formatBytes(zipInfo.size)})。`);
|
|
324
|
+
} else {
|
|
325
|
+
info("第 6 步:准备插件包(下载或复用缓存)…");
|
|
326
|
+
const archive = await ensurePluginArchive({
|
|
327
|
+
archiveUrl: plugin.archiveUrl,
|
|
328
|
+
sha256: plugin.sha256,
|
|
329
|
+
size: plugin.size,
|
|
330
|
+
});
|
|
331
|
+
zipInfo = await validatePluginZip(archive.zipPath, {
|
|
332
|
+
expectedSha256: plugin.sha256,
|
|
333
|
+
expectedSize: plugin.size,
|
|
334
|
+
expectedVersion: plugin.pluginVersion,
|
|
335
|
+
});
|
|
336
|
+
info(
|
|
337
|
+
`插件包结构校验通过:${zipInfo.entryCount} 个条目 / ${zipInfo.fileCount} 个文件,` +
|
|
338
|
+
`插件版本 ${zipInfo.pluginVersion}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
info("第 7 步:解压插件…");
|
|
343
|
+
const succeeded = [];
|
|
344
|
+
const failed = [];
|
|
345
|
+
for (const { target } of pending) {
|
|
346
|
+
info(`→ ${target.pluginsDir}`);
|
|
347
|
+
try {
|
|
348
|
+
const result = await extractPluginZip({
|
|
349
|
+
zipPath: zipInfo.zipPath,
|
|
350
|
+
pluginsDir: target.pluginsDir,
|
|
351
|
+
expectedEntries: zipInfo.entryCount,
|
|
352
|
+
expectedBytes: zipInfo.totalBytes,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const installedJar = await findInstalledJar(result.pluginDir);
|
|
356
|
+
if (!installedJar) {
|
|
357
|
+
throw new InstallError("解压后未找到 lib/zcode-ide-*.jar,安装可能不完整。");
|
|
358
|
+
}
|
|
359
|
+
if (
|
|
360
|
+
zipInfo.hasRuntime &&
|
|
361
|
+
!(await pathExists(path.join(result.pluginDir, "runtime", "web", "index.html")))
|
|
362
|
+
) {
|
|
363
|
+
throw new InstallError("解压后缺少 runtime/web/index.html,安装可能不完整。");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
info(`安装完成:${result.pluginDir}(${result.files} 个文件,${formatBytes(result.bytes)})`);
|
|
367
|
+
succeeded.push({ target, result, jar: installedJar });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
warn(`安装失败:${target.pluginsDir} — ${err.message}`);
|
|
370
|
+
failed.push({ target, error: err });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* ------------------------------ 7. 汇总 + 启动 ------------------------------ */
|
|
375
|
+
info("第 8 步:安装汇总。");
|
|
376
|
+
if (succeeded.length > 0) {
|
|
377
|
+
info(`成功 ${succeeded.length} 个:`);
|
|
378
|
+
for (const { target, result } of succeeded) {
|
|
379
|
+
info(` - ${formatTarget(target)} → ${result.pluginDir}(${result.files} 个文件)`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (failed.length > 0) {
|
|
383
|
+
warn(`失败 ${failed.length} 个:`);
|
|
384
|
+
for (const { target, error: failure } of failed) {
|
|
385
|
+
warn(` - ${target.pluginsDir}:${failure.message}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (succeeded.length > 0 && !noLaunch) {
|
|
390
|
+
info("提示:首次打开面板会自动下载 runtime(薄包)或直接使用内置 runtime(完整包)。");
|
|
391
|
+
for (const { target } of succeeded) {
|
|
392
|
+
const executable = findLaunchCommand(target);
|
|
393
|
+
if (!executable) {
|
|
394
|
+
warn(`未找到 ${target.productName} 的可执行文件,请手动打开 IDE。`);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
await launchDetached(executable);
|
|
398
|
+
}
|
|
399
|
+
} else if (noLaunch) {
|
|
400
|
+
info("已按 --no-launch 跳过启动。");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (cleanCache) {
|
|
404
|
+
if (succeeded.length > 0) {
|
|
405
|
+
// 清理是收尾动作:失败只告警,不能改变安装结果与退出码。
|
|
406
|
+
try {
|
|
407
|
+
const pruned = await prunePluginCache({ keep: cacheKeep });
|
|
408
|
+
info(
|
|
409
|
+
`缓存清理完成:删除 ${pruned.removed.length} 个旧缓存,` +
|
|
410
|
+
`释放 ${formatBytes(pruned.freedBytes)}${cacheKeepNote}。`,
|
|
411
|
+
);
|
|
412
|
+
for (const item of pruned.failed) {
|
|
413
|
+
warn(`缓存清理失败:${item.path} — ${item.error}`);
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
warn(`缓存清理失败,已跳过:${err.message}`);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
// 全部目标安装失败时保留旧缓存,方便回滚/重试。
|
|
420
|
+
warn("安装失败,已跳过缓存清理。");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (failed.length > 0) {
|
|
425
|
+
error(`${failed.length} 个目标安装失败,请查看上面的错误信息。`);
|
|
426
|
+
return 1;
|
|
427
|
+
}
|
|
428
|
+
info("全部完成。请在 IDE 中打开 ZCode 工具窗口(View → Tool Windows → ZCode)。");
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function pathExists(target) {
|
|
433
|
+
try {
|
|
434
|
+
await fsp.access(target);
|
|
435
|
+
return true;
|
|
436
|
+
} catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function findInstalledJar(pluginDir) {
|
|
442
|
+
try {
|
|
443
|
+
const entries = await fsp.readdir(path.join(pluginDir, "lib"));
|
|
444
|
+
return entries.find((name) => /^zcode-ide-.*\.jar$/i.test(name)) ?? null;
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
main()
|
|
451
|
+
.then((code) => {
|
|
452
|
+
process.exitCode = code;
|
|
453
|
+
})
|
|
454
|
+
.catch((err) => {
|
|
455
|
+
if (err && (err.name === "ExitPromptError" || err.name === "AbortPromptError")) {
|
|
456
|
+
warn("已取消。");
|
|
457
|
+
process.exitCode = 130;
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
error(err?.message || String(err));
|
|
461
|
+
if (err instanceof UsageError) info("用 --help 查看用法。");
|
|
462
|
+
process.exitCode = err instanceof UsageError ? 2 : 1;
|
|
463
|
+
});
|