@delta-infra/cli 1.0.26 → 1.0.28
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 +28 -4
- package/checksums.txt +5 -5
- package/package.json +5 -1
- package/scripts/install-wizard.js +403 -0
- package/scripts/install.js +230 -299
- package/scripts/run.js +54 -49
package/README.md
CHANGED
|
@@ -4,7 +4,31 @@ Delta Infra 的命令行工具。
|
|
|
4
4
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### 方式一:npx 一键安装(推荐)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @delta-infra/cli@latest install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
该命令会依次完成:
|
|
14
|
+
1. **全局安装** `@delta-infra/cli`(含 Go 二进制)
|
|
15
|
+
2. **安装 AI Skills** 到 `~/.agents/skills/`,自动适配 Claude Code、Codex、OpenCode、Cursor 等 AI 工具
|
|
16
|
+
3. **初始化配置** `delta-cli config init`(交互式,非 TTY 跳过)
|
|
17
|
+
4. **身份认证** `delta-cli auth login`(交互式,非 TTY 跳过)
|
|
18
|
+
|
|
19
|
+
AI Agent 也可以直接使用以下指令完成安装:
|
|
20
|
+
|
|
21
|
+
> 帮我安装 delta-cli:https://github.com/yzailab/delta-infra-cli
|
|
22
|
+
|
|
23
|
+
### 卸载
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @delta-infra/cli@latest uninstall
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
清理全局包、AI Skills 和配置文件。
|
|
30
|
+
|
|
31
|
+
### 方式二:直接下载 Binary
|
|
8
32
|
|
|
9
33
|
从 [GitHub Releases](https://github.com/yzailab/delta-infra-cli/releases) 下载对应平台的压缩包(以下使用 `latest` 指向最新版):
|
|
10
34
|
|
|
@@ -30,7 +54,7 @@ Remove-Item -Path delta-cli.zip
|
|
|
30
54
|
# 将 delta-cli.exe 放到 PATH 中的目录
|
|
31
55
|
```
|
|
32
56
|
|
|
33
|
-
###
|
|
57
|
+
### 方式三:一键安装脚本(国内用户推荐)
|
|
34
58
|
|
|
35
59
|
安装脚本会自动尝试国内 npm 镜像(npmmirror),失败后再回退到 npm 官方源:
|
|
36
60
|
|
|
@@ -42,7 +66,7 @@ curl -L https://raw.githubusercontent.com/yzailab/delta-infra-cli/main/install.s
|
|
|
42
66
|
Invoke-RestMethod -Uri https://raw.githubusercontent.com/yzailab/delta-infra-cli/main/install.ps1 | Invoke-Expression
|
|
43
67
|
```
|
|
44
68
|
|
|
45
|
-
###
|
|
69
|
+
### 方式四:npm
|
|
46
70
|
|
|
47
71
|
```bash
|
|
48
72
|
npm install -g @delta-infra/cli
|
|
@@ -74,7 +98,7 @@ DELTA_CLI_ARCHIVE=/path/to/delta-cli-linux-amd64.tar.gz npm install -g @delta-in
|
|
|
74
98
|
| `DELTA_CLI_FATAL_ON_ERROR` | 安装失败时退出码非 0(默认警告,用于调试)。 |
|
|
75
99
|
| `DELTA_CLI_DOWNLOAD_TIMEOUT` | 单个下载源最大等待时间(毫秒,默认 120000)。 |
|
|
76
100
|
|
|
77
|
-
###
|
|
101
|
+
### 方式五:Go Install
|
|
78
102
|
|
|
79
103
|
```bash
|
|
80
104
|
go install github.com/delta-infra/delta-infra-cli/cmd/delta-cli@latest
|
package/checksums.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
79b5433250ec9697d0a7c360eb4b35c9f27e62152de32fad89a905e374bd29e0 delta-cli-darwin-amd64.tar.gz
|
|
2
|
+
0602d8f72b91de73270ec0f385202f543649375e948719641179a78d1f37fdcf delta-cli-darwin-arm64.tar.gz
|
|
3
|
+
6db533d7826c62dc411c71c0a455cf37c5d580528b8fc5bc0e17904d9f00ffc2 delta-cli-linux-amd64.tar.gz
|
|
4
|
+
0ddb97549520f524079885aab54e2058aad97fdbc9014a123d17fa91319f8280 delta-cli-linux-arm64.tar.gz
|
|
5
|
+
7eab714f28f6ef0a1e713a7f98d2203975c5249061bbb37a60ee387398369451 delta-cli-windows-amd64.zip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delta-infra/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "Delta Sandbox CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"delta-cli": "scripts/run.js"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"checksums.txt",
|
|
13
13
|
"scripts/install.js",
|
|
14
|
+
"scripts/install-wizard.js",
|
|
14
15
|
"scripts/run.js",
|
|
15
16
|
"skills/delta-sandbox/SKILL.md",
|
|
16
17
|
"skills/delta-sandbox/references/",
|
|
@@ -40,5 +41,8 @@
|
|
|
40
41
|
"repository": {
|
|
41
42
|
"type": "git",
|
|
42
43
|
"url": "git+https://github.com/yzailab/delta-infra-cli.git"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@clack/prompts": "^1.2.0"
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 Delta Infra Authors
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execFileSync, execFile } = require("child_process");
|
|
8
|
+
const p = require("@clack/prompts");
|
|
9
|
+
|
|
10
|
+
const PKG = "@delta-infra/cli";
|
|
11
|
+
const SKILLS_REPO = "yzailab/delta-infra-cli";
|
|
12
|
+
const CONFIG_DIR = path.join(osHomedir(), ".delta-infra");
|
|
13
|
+
const isWindows = process.platform === "win32";
|
|
14
|
+
|
|
15
|
+
// ── i18n ────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const messages = {
|
|
18
|
+
zh: {
|
|
19
|
+
setup: "正在设置 Delta Sandbox CLI...",
|
|
20
|
+
upgrade: "正在升级 %s (v%s → v%s)...",
|
|
21
|
+
step1: "正在安装 %s...",
|
|
22
|
+
step1Skip: "已安装 (v%s),跳过",
|
|
23
|
+
step1Done: "已全局安装",
|
|
24
|
+
step1Upgraded: "已升级到 v%s",
|
|
25
|
+
step1Fail: "全局安装失败。运行以下命令重试: npm install -g %s",
|
|
26
|
+
step2: "安装 AI Skills",
|
|
27
|
+
step2Spinner: "正在安装 Skills...",
|
|
28
|
+
step2Done: "Skills 已安装",
|
|
29
|
+
step2Skip: "已安装,跳过",
|
|
30
|
+
step2Fail: "Skills 安装失败。运行以下命令重试: npx skills add %s -y -g",
|
|
31
|
+
step3: "正在初始化配置...",
|
|
32
|
+
step3Skip: "跳过配置初始化",
|
|
33
|
+
step3Done: "配置已初始化",
|
|
34
|
+
step3Fail: "配置初始化失败。运行以下命令重试: delta-cli config init",
|
|
35
|
+
step4: "身份认证",
|
|
36
|
+
step4Confirm: "是否进行身份认证?",
|
|
37
|
+
step4Skip: "跳过身份认证。后续运行 delta-cli auth login 完成",
|
|
38
|
+
step4Done: "认证完成",
|
|
39
|
+
step4Fail: "认证失败。运行以下命令重试: delta-cli auth login",
|
|
40
|
+
done: "安装完成!\n现在可以和你的 AI 工具说:\"帮我使用 delta sandbox 运行一个 GPU 任务\"",
|
|
41
|
+
cancelled: "安装已取消",
|
|
42
|
+
nonTtyHint: "要完成配置,请在终端中运行:\n delta-cli config init\n delta-cli auth login",
|
|
43
|
+
},
|
|
44
|
+
en: {
|
|
45
|
+
setup: "Setting up Delta Sandbox CLI...",
|
|
46
|
+
upgrade: "Upgrading %s (v%s → v%s)...",
|
|
47
|
+
step1: "Installing %s globally...",
|
|
48
|
+
step1Skip: "Already installed (v%s). Skipped",
|
|
49
|
+
step1Done: "Installed globally",
|
|
50
|
+
step1Upgraded: "Upgraded to v%s",
|
|
51
|
+
step1Fail: "Failed to install globally. Run manually: npm install -g %s",
|
|
52
|
+
step2: "Install AI skills",
|
|
53
|
+
step2Spinner: "Installing skills...",
|
|
54
|
+
step2Done: "Skills installed",
|
|
55
|
+
step2Skip: "Already installed. Skipped",
|
|
56
|
+
step2Fail: "Failed to install skills. Run manually: npx skills add %s -y -g",
|
|
57
|
+
step3: "Initializing config...",
|
|
58
|
+
step3Skip: "Skipped config initialization",
|
|
59
|
+
step3Done: "Config initialized",
|
|
60
|
+
step3Fail: "Failed to init config. Run manually: delta-cli config init",
|
|
61
|
+
step4: "Authorization",
|
|
62
|
+
step4Confirm: "Would you like to authenticate now?",
|
|
63
|
+
step4Skip: "Skipped. Run delta-cli auth login to authorize later",
|
|
64
|
+
step4Done: "Authorization complete",
|
|
65
|
+
step4Fail: "Failed to authorize. Run delta-cli auth login to retry",
|
|
66
|
+
done: "You are all set!\nNow try asking your AI tool: \"Help me run a GPU task with delta sandbox\"",
|
|
67
|
+
cancelled: "Installation cancelled",
|
|
68
|
+
nonTtyHint: "To complete setup, run interactively:\n delta-cli config init\n delta-cli auth login",
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function handleCancel(value, msg) {
|
|
75
|
+
if (p.isCancel(value)) {
|
|
76
|
+
p.cancel(msg.cancelled);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function execCmd(cmd, args, opts) {
|
|
83
|
+
if (isWindows) {
|
|
84
|
+
return execFileSync("cmd.exe", ["/c", cmd, ...args], opts);
|
|
85
|
+
}
|
|
86
|
+
return execFileSync(cmd, args, opts);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function run(cmd, args, opts = {}) {
|
|
90
|
+
execCmd(cmd, args, { stdio: "inherit", ...opts });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runSilent(cmd, args, opts = {}) {
|
|
94
|
+
return execCmd(cmd, args, {
|
|
95
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
96
|
+
...opts,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runSilentAsync(cmd, args, opts = {}) {
|
|
101
|
+
const actualCmd = isWindows ? "cmd.exe" : cmd;
|
|
102
|
+
const actualArgs = isWindows ? ["/c", cmd, ...args] : args;
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
execFile(actualCmd, actualArgs, {
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
...opts,
|
|
107
|
+
}, (err, stdout) => {
|
|
108
|
+
if (err) reject(err);
|
|
109
|
+
else resolve(stdout);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fmt(template, ...values) {
|
|
115
|
+
let i = 0;
|
|
116
|
+
return template.replace(/%s/g, () => values[i++] ?? "");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function osHomedir() {
|
|
120
|
+
return process.env.HOME ||
|
|
121
|
+
process.env.USERPROFILE ||
|
|
122
|
+
(process.env.HOMEDRIVE && process.env.HOMEPATH && process.env.HOMEDRIVE + process.env.HOMEPATH) ||
|
|
123
|
+
"/root";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function whichDeltaCli() {
|
|
127
|
+
try {
|
|
128
|
+
const prefix = execFileSync("npm", ["prefix", "-g"], {
|
|
129
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
130
|
+
}).toString().trim();
|
|
131
|
+
const bin = isWindows
|
|
132
|
+
? path.join(prefix, "delta-cli.cmd")
|
|
133
|
+
: path.join(prefix, "bin", "delta-cli");
|
|
134
|
+
if (fs.existsSync(bin)) return bin;
|
|
135
|
+
} catch {}
|
|
136
|
+
try {
|
|
137
|
+
const cmd = isWindows ? "where" : "which";
|
|
138
|
+
return execFileSync(cmd, ["delta-cli"], { stdio: ["ignore", "pipe", "pipe"] })
|
|
139
|
+
.toString().split("\n")[0].trim();
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getLatestVersion() {
|
|
146
|
+
try {
|
|
147
|
+
const out = runSilent("npm", ["view", PKG, "version"], { timeout: 15000 });
|
|
148
|
+
const ver = out.toString().trim();
|
|
149
|
+
return /^\d+\.\d+\.\d+/.test(ver) ? ver : null;
|
|
150
|
+
} catch { return null; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function semverLessThan(a, b) {
|
|
154
|
+
const pa = a.replace(/-.*$/, "").split(".").map(Number);
|
|
155
|
+
const pb = b.replace(/-.*$/, "").split(".").map(Number);
|
|
156
|
+
for (let i = 0; i < 3; i++) {
|
|
157
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return true;
|
|
158
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return false;
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getGloballyInstalledVersion() {
|
|
164
|
+
try {
|
|
165
|
+
const out = runSilent("npm", ["list", "-g", PKG], { timeout: 15000 });
|
|
166
|
+
const match = out.toString().match(/@(\d+\.\d+\.\d+[^\s]*)/);
|
|
167
|
+
return match ? match[1] : "unknown";
|
|
168
|
+
} catch { return null; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getExistingConfig() {
|
|
172
|
+
try {
|
|
173
|
+
const configPath = path.join(CONFIG_DIR, "config.json");
|
|
174
|
+
if (!fs.existsSync(configPath)) return null;
|
|
175
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
176
|
+
} catch { return null; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Steps ───────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
async function stepInstallGlobally(msg) {
|
|
182
|
+
const installedVer = getGloballyInstalledVersion();
|
|
183
|
+
const latestVer = getLatestVersion();
|
|
184
|
+
const needsUpgrade = installedVer && latestVer && semverLessThan(installedVer, latestVer);
|
|
185
|
+
|
|
186
|
+
if (installedVer && !needsUpgrade) {
|
|
187
|
+
p.log.info(fmt(msg.step1Skip, installedVer));
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const s = p.spinner();
|
|
192
|
+
if (needsUpgrade) {
|
|
193
|
+
s.start(fmt(msg.upgrade, PKG, installedVer, latestVer));
|
|
194
|
+
} else {
|
|
195
|
+
s.start(fmt(msg.step1, PKG));
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
await runSilentAsync("npm", ["install", "-g", PKG], { timeout: 120000 });
|
|
199
|
+
s.stop(needsUpgrade ? fmt(msg.step1Upgraded, latestVer) : msg.step1Done);
|
|
200
|
+
return needsUpgrade;
|
|
201
|
+
} catch {
|
|
202
|
+
s.stop(fmt(msg.step1Fail, PKG));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function skillsAlreadyInstalled() {
|
|
208
|
+
try {
|
|
209
|
+
const out = await runSilentAsync("npx", ["-y", "skills", "ls", "-g"], {
|
|
210
|
+
timeout: 120000,
|
|
211
|
+
});
|
|
212
|
+
return /^delta-/m.test(out.toString());
|
|
213
|
+
} catch { return false; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function stepInstallSkills(msg) {
|
|
217
|
+
const s = p.spinner();
|
|
218
|
+
s.start(msg.step2Spinner);
|
|
219
|
+
try {
|
|
220
|
+
if (await skillsAlreadyInstalled()) {
|
|
221
|
+
s.stop(msg.step2Skip);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
await runSilentAsync("npx", ["-y", "skills", "add", SKILLS_REPO, "-y", "-g"], {
|
|
226
|
+
timeout: 120000,
|
|
227
|
+
});
|
|
228
|
+
} catch {
|
|
229
|
+
// Retry with GitHub URL directly
|
|
230
|
+
await runSilentAsync("npx", [
|
|
231
|
+
"-y", "skills", "add",
|
|
232
|
+
`https://github.com/${SKILLS_REPO}`, "-y", "-g",
|
|
233
|
+
], { timeout: 120000 });
|
|
234
|
+
}
|
|
235
|
+
s.stop(msg.step2Done);
|
|
236
|
+
} catch {
|
|
237
|
+
s.stop(fmt(msg.step2Fail, SKILLS_REPO));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function stepConfigInit(msg) {
|
|
243
|
+
const s = p.spinner();
|
|
244
|
+
s.start(msg.step3);
|
|
245
|
+
|
|
246
|
+
const deltaCli = whichDeltaCli();
|
|
247
|
+
if (!deltaCli) {
|
|
248
|
+
s.stop(msg.step3);
|
|
249
|
+
p.log.warn("delta-cli not found on PATH after global install.");
|
|
250
|
+
p.log.info(msg.step3Fail);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check if config already exists with valid data
|
|
255
|
+
const existingConfig = getExistingConfig();
|
|
256
|
+
s.stop(msg.step3);
|
|
257
|
+
|
|
258
|
+
if (existingConfig && existingConfig.base_url) {
|
|
259
|
+
const reuse = await p.confirm({
|
|
260
|
+
message: `发现已有配置 (Server: ${existingConfig.base_url}),继续使用?`,
|
|
261
|
+
});
|
|
262
|
+
if (handleCancel(reuse, msg) && reuse) {
|
|
263
|
+
p.log.info(msg.step3Skip);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
run(deltaCli, ["config", "init"]);
|
|
270
|
+
p.log.success(msg.step3Done);
|
|
271
|
+
} catch {
|
|
272
|
+
p.log.error(msg.step3Fail);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function stepAuthLogin(msg) {
|
|
278
|
+
const deltaCli = whichDeltaCli();
|
|
279
|
+
if (!deltaCli) {
|
|
280
|
+
p.log.warn("delta-cli not found, skipping authorization");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const yes = await p.confirm({
|
|
285
|
+
message: msg.step4Confirm,
|
|
286
|
+
});
|
|
287
|
+
if (p.isCancel(yes)) {
|
|
288
|
+
p.cancel(msg.cancelled);
|
|
289
|
+
process.exit(0);
|
|
290
|
+
}
|
|
291
|
+
if (!yes) {
|
|
292
|
+
p.log.info(msg.step4Skip);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
p.log.step(msg.step4);
|
|
297
|
+
try {
|
|
298
|
+
run(deltaCli, ["auth", "login"]);
|
|
299
|
+
p.log.success(msg.step4Done);
|
|
300
|
+
} catch {
|
|
301
|
+
p.log.warn(msg.step4Fail);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Uninstall steps ─────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
async function stepUninstallSkills(msg) {
|
|
308
|
+
const s = p.spinner();
|
|
309
|
+
s.start("Removing AI skills...");
|
|
310
|
+
try {
|
|
311
|
+
await runSilentAsync("npx", ["-y", "skills", "remove", "delta-sandbox", "delta-shared", "-g"], {
|
|
312
|
+
timeout: 120000,
|
|
313
|
+
});
|
|
314
|
+
s.stop("AI skills removed");
|
|
315
|
+
} catch {
|
|
316
|
+
s.stop("Warning: skill removal failed. You can retry:");
|
|
317
|
+
p.log.info(" npx skills remove delta-sandbox delta-shared -g");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function stepRemoveConfig() {
|
|
322
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function stepUninstallPackage(msg) {
|
|
329
|
+
const installedVer = getGloballyInstalledVersion();
|
|
330
|
+
if (!installedVer) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const s = p.spinner();
|
|
334
|
+
s.start("Removing global package...");
|
|
335
|
+
try {
|
|
336
|
+
await runSilentAsync("npm", ["uninstall", "-g", PKG], { timeout: 60000 });
|
|
337
|
+
s.stop("Package removed");
|
|
338
|
+
} catch {
|
|
339
|
+
s.stop("Failed to remove global package");
|
|
340
|
+
p.log.info(` npm uninstall -g ${PKG}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
async function doInstall() {
|
|
347
|
+
const isInteractive = !!process.stdin.isTTY;
|
|
348
|
+
|
|
349
|
+
if (isInteractive) {
|
|
350
|
+
p.intro(messages.zh.setup);
|
|
351
|
+
await stepInstallGlobally(messages.zh);
|
|
352
|
+
await stepInstallSkills(messages.zh);
|
|
353
|
+
await stepConfigInit(messages.zh);
|
|
354
|
+
await stepAuthLogin(messages.zh);
|
|
355
|
+
p.outro(messages.zh.done);
|
|
356
|
+
} else {
|
|
357
|
+
console.log(messages.zh.setup);
|
|
358
|
+
await stepInstallGlobally(messages.zh);
|
|
359
|
+
await stepInstallSkills(messages.zh);
|
|
360
|
+
console.log(messages.zh.nonTtyHint);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function doUninstall() {
|
|
365
|
+
const isInteractive = !!process.stdin.isTTY;
|
|
366
|
+
|
|
367
|
+
if (isInteractive) {
|
|
368
|
+
const ok = await p.confirm({
|
|
369
|
+
message: "将移除全局包、AI Skills 和配置文件。是否继续?",
|
|
370
|
+
});
|
|
371
|
+
if (p.isCancel(ok) || !ok) {
|
|
372
|
+
p.cancel("卸载已取消");
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await stepRemoveConfig();
|
|
378
|
+
await stepUninstallSkills();
|
|
379
|
+
await stepUninstallPackage();
|
|
380
|
+
|
|
381
|
+
if (isInteractive) {
|
|
382
|
+
p.outro("卸载完成");
|
|
383
|
+
} else {
|
|
384
|
+
console.log("\nUninstall complete.");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function main() {
|
|
389
|
+
const subcommand = process.argv[2];
|
|
390
|
+
if (subcommand === "uninstall") {
|
|
391
|
+
doUninstall().catch((e) => {
|
|
392
|
+
console.error("[delta-cli] Uninstall failed:", e.message);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
doInstall().catch((e) => {
|
|
397
|
+
console.error("[delta-cli] Install failed:", e.message);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
main();
|
package/scripts/install.js
CHANGED
|
@@ -1,368 +1,299 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
// Copyright (c) 2026 Delta Infra Authors
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execFileSync } = require("child_process");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const crypto = require("crypto");
|
|
9
|
+
|
|
10
|
+
const VERSION = require("../package.json").version.replace(/-.*$/, "");
|
|
11
|
+
const NAME = "delta-cli";
|
|
12
|
+
const REPO = "yzailab/delta-infra-cli";
|
|
13
|
+
const DEFAULT_ALLOWED_HOSTS = new Set([
|
|
14
|
+
"github.com",
|
|
15
|
+
"gh-proxy.com",
|
|
16
|
+
"objects.githubusercontent.com",
|
|
17
|
+
"release-assets.githubusercontent.com",
|
|
18
|
+
"github-releases.githubusercontent.com",
|
|
19
|
+
"raw.githubusercontent.com",
|
|
20
|
+
"registry.npmmirror.com",
|
|
21
|
+
]);
|
|
19
22
|
const DEFAULT_ALLOWED_HOST_PREFIXES = [
|
|
20
|
-
|
|
23
|
+
"github-production-release-asset-",
|
|
21
24
|
];
|
|
22
|
-
const MAX_REDIRECTS = 5;
|
|
23
25
|
|
|
24
|
-
const
|
|
25
|
-
darwin:
|
|
26
|
-
linux:
|
|
27
|
-
win32:
|
|
26
|
+
const PLATFORM_MAP = {
|
|
27
|
+
darwin: "darwin",
|
|
28
|
+
linux: "linux",
|
|
29
|
+
win32: "windows",
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
x64:
|
|
32
|
-
arm64:
|
|
32
|
+
const ARCH_MAP = {
|
|
33
|
+
x64: "amd64",
|
|
34
|
+
arm64: "arm64",
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
+
const platform = PLATFORM_MAP[process.platform];
|
|
38
|
+
const arch = ARCH_MAP[process.arch];
|
|
39
|
+
|
|
40
|
+
const isWindows = process.platform === "win32";
|
|
41
|
+
const ext = isWindows ? ".zip" : ".tar.gz";
|
|
42
|
+
const archiveName = `${NAME}-${platform}-${arch}${ext}`;
|
|
43
|
+
const GITHUB_URL = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
|
|
44
|
+
|
|
45
|
+
const binDir = path.join(__dirname, "..", "bin");
|
|
46
|
+
const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
|
|
47
|
+
|
|
48
|
+
// ── Mirror URL resolution ──────────────────────────────────────────────────
|
|
37
49
|
|
|
38
50
|
function releaseAssetUrls(version, assetName) {
|
|
39
51
|
const urls = [];
|
|
40
|
-
const mirror = process.env.DELTA_CLI_MIRROR ||
|
|
52
|
+
const mirror = process.env.DELTA_CLI_MIRROR || "";
|
|
41
53
|
if (mirror) {
|
|
42
|
-
urls.push(`${mirror.replace(/\/$/,
|
|
54
|
+
urls.push(`${mirror.replace(/\/$/, "")}/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`);
|
|
43
55
|
}
|
|
44
|
-
|
|
45
|
-
urls.push(
|
|
56
|
+
// China acceleration mirror (gh-proxy.com)
|
|
57
|
+
urls.push(`https://gh-proxy.com/https://github.com/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`);
|
|
58
|
+
// GitHub source
|
|
59
|
+
urls.push(GITHUB_URL);
|
|
46
60
|
return urls;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
if (location.startsWith('http://') || location.startsWith('https://')) {
|
|
52
|
-
return location;
|
|
53
|
-
}
|
|
54
|
-
const baseUrl = new URL(base);
|
|
55
|
-
return new URL(location, baseUrl).href;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function loadChecksums() {
|
|
59
|
-
const checksumPath = path.join(__dirname, '..', 'checksums.txt');
|
|
60
|
-
if (!fs.existsSync(checksumPath)) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const content = fs.readFileSync(checksumPath, 'utf8');
|
|
64
|
-
const map = new Map();
|
|
65
|
-
for (const line of content.split(/\r?\n/)) {
|
|
66
|
-
const trimmed = line.trim();
|
|
67
|
-
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
68
|
-
const idx = trimmed.indexOf(' ');
|
|
69
|
-
let hash, filename;
|
|
70
|
-
if (idx === -1) {
|
|
71
|
-
const parts = trimmed.split(/\s+/);
|
|
72
|
-
if (parts.length < 2) continue;
|
|
73
|
-
hash = parts[0];
|
|
74
|
-
filename = parts[1];
|
|
75
|
-
} else {
|
|
76
|
-
hash = trimmed.slice(0, idx);
|
|
77
|
-
filename = trimmed.slice(idx + 2);
|
|
78
|
-
}
|
|
79
|
-
map.set(filename, hash.toLowerCase());
|
|
80
|
-
}
|
|
81
|
-
return map;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function fileSha256(filePath) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
const hash = crypto.createHash('sha256');
|
|
87
|
-
const stream = fs.createReadStream(filePath);
|
|
88
|
-
stream.on('data', (chunk) => hash.update(chunk));
|
|
89
|
-
stream.on('end', () => resolve(hash.digest('hex')));
|
|
90
|
-
stream.on('error', reject);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function isAllowListedUrl(urlStr) {
|
|
95
|
-
let url;
|
|
96
|
-
try {
|
|
97
|
-
url = new URL(urlStr);
|
|
98
|
-
} catch {
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
if (url.protocol !== 'https:') {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
const host = url.hostname.toLowerCase();
|
|
63
|
+
function assertAllowedHost(url) {
|
|
64
|
+
const { hostname } = new URL(url);
|
|
105
65
|
const allowlist = new Set(DEFAULT_ALLOWED_HOSTS);
|
|
106
|
-
(process.env.DELTA_CLI_MIRROR_ALLOWLIST ||
|
|
107
|
-
.split(
|
|
66
|
+
(process.env.DELTA_CLI_MIRROR_ALLOWLIST || "")
|
|
67
|
+
.split(",")
|
|
108
68
|
.map((h) => h.trim().toLowerCase())
|
|
109
69
|
.filter(Boolean)
|
|
110
70
|
.forEach((h) => allowlist.add(h));
|
|
111
71
|
for (const allowed of allowlist) {
|
|
112
|
-
if (
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
72
|
+
if (hostname === allowed || hostname.endsWith("." + allowed)) return;
|
|
115
73
|
}
|
|
116
74
|
for (const prefix of DEFAULT_ALLOWED_HOST_PREFIXES) {
|
|
117
|
-
if (
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
75
|
+
if (hostname.startsWith(prefix)) return;
|
|
120
76
|
}
|
|
121
|
-
|
|
77
|
+
throw new Error(`Download host not allowed: ${hostname}`);
|
|
122
78
|
}
|
|
123
79
|
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
process.env.HTTPS_PROXY ||
|
|
127
|
-
process.env.https_proxy ||
|
|
128
|
-
process.env.HTTP_PROXY ||
|
|
129
|
-
process.env.http_proxy ||
|
|
130
|
-
''
|
|
131
|
-
);
|
|
132
|
-
}
|
|
80
|
+
// ── curl version detection (for --ssl-revoke-best-effort) ──────────────────
|
|
133
81
|
|
|
134
|
-
|
|
82
|
+
function isCurlVersionSupported(versionOutput) {
|
|
83
|
+
const match = String(versionOutput).match(/^\s*curl\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
84
|
+
if (!match) return false;
|
|
85
|
+
const major = parseInt(match[1], 10);
|
|
86
|
+
const minor = parseInt(match[2], 10);
|
|
87
|
+
return major > 7 || (major === 7 && minor >= 70);
|
|
88
|
+
}
|
|
135
89
|
|
|
136
|
-
|
|
137
|
-
return new Promise((resolve, reject) => {
|
|
138
|
-
if (!isAllowListedUrl(url)) {
|
|
139
|
-
reject(new Error(`Download URL is not allow-listed: ${url}`));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (redirectCount > MAX_REDIRECTS) {
|
|
143
|
-
reject(new Error('Too many redirects while downloading'));
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
90
|
+
let _curlSupportsSslRevokeBestEffort;
|
|
146
91
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
reject(err);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
https
|
|
166
|
-
.get(url, { timeout: 60000 }, (response) => {
|
|
167
|
-
if (response.statusCode >= 301 && response.statusCode <= 308 && response.headers.location) {
|
|
168
|
-
const nextUrl = resolveUrl(url, response.headers.location);
|
|
169
|
-
if (!isAllowListedUrl(nextUrl)) {
|
|
170
|
-
cleanup(new Error(`Redirect URL is not allow-listed: ${nextUrl}`));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
file.destroy();
|
|
174
|
-
try {
|
|
175
|
-
fs.unlinkSync(dest);
|
|
176
|
-
} catch {}
|
|
177
|
-
nodeDownload(nextUrl, dest, redirectCount + 1).then(resolve).catch(reject);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (response.statusCode !== 200) {
|
|
181
|
-
cleanup(new Error(`Download failed: HTTP ${response.statusCode}`));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
response.pipe(file);
|
|
185
|
-
response.on('data', (chunk) => hash.update(chunk));
|
|
186
|
-
file.on('finish', () => {
|
|
187
|
-
file.close((err) => {
|
|
188
|
-
if (finished) return;
|
|
189
|
-
if (err) {
|
|
190
|
-
cleanup(err);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
finished = true;
|
|
194
|
-
clearTimeout(maxTimer);
|
|
195
|
-
resolve(hash.digest('hex'));
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
file.on('error', cleanup);
|
|
199
|
-
})
|
|
200
|
-
.on('error', cleanup)
|
|
201
|
-
.on('timeout', () => {
|
|
202
|
-
cleanup(new Error('Download timed out'));
|
|
203
|
-
});
|
|
204
|
-
});
|
|
92
|
+
function curlSupportsSslRevokeBestEffort() {
|
|
93
|
+
if (_curlSupportsSslRevokeBestEffort !== undefined) {
|
|
94
|
+
return _curlSupportsSslRevokeBestEffort;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const output = execFileSync("curl", ["--version"], {
|
|
98
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
timeout: 5000,
|
|
101
|
+
});
|
|
102
|
+
_curlSupportsSslRevokeBestEffort = isCurlVersionSupported(output);
|
|
103
|
+
} catch {
|
|
104
|
+
_curlSupportsSslRevokeBestEffort = false;
|
|
105
|
+
}
|
|
106
|
+
return _curlSupportsSslRevokeBestEffort;
|
|
205
107
|
}
|
|
206
108
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
fileSha256(dest).then(resolve).catch(reject);
|
|
225
|
-
});
|
|
226
|
-
});
|
|
109
|
+
// ── Download ───────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function download(url, destPath) {
|
|
112
|
+
assertAllowedHost(url);
|
|
113
|
+
const args = [
|
|
114
|
+
"--fail", "--location", "--silent", "--show-error",
|
|
115
|
+
"--connect-timeout", "10", "--max-time", "120",
|
|
116
|
+
"--max-redirs", "3",
|
|
117
|
+
"--output", destPath,
|
|
118
|
+
];
|
|
119
|
+
if (isWindows && curlSupportsSslRevokeBestEffort()) {
|
|
120
|
+
args.unshift("--ssl-revoke-best-effort");
|
|
121
|
+
}
|
|
122
|
+
args.push(url);
|
|
123
|
+
execFileSync("curl", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
227
124
|
}
|
|
228
125
|
|
|
229
|
-
|
|
230
|
-
const proxy = getHttpProxy();
|
|
231
|
-
const preferCurl =
|
|
232
|
-
process.env.DELTA_CLI_USE_CURL === '1' || (!!proxy && process.env.DELTA_CLI_USE_CURL !== '0');
|
|
126
|
+
// ── Extraction ─────────────────────────────────────────────────────────────
|
|
233
127
|
|
|
234
|
-
|
|
128
|
+
function extractZipWindows(archivePath, destDir) {
|
|
129
|
+
const psOpts = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"];
|
|
130
|
+
const psEnv = {
|
|
131
|
+
DELTA_CLI_ARCHIVE: archivePath,
|
|
132
|
+
DELTA_CLI_DEST: destDir,
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
const dotnet =
|
|
136
|
+
"$ErrorActionPreference='Stop';" +
|
|
137
|
+
"Add-Type -AssemblyName System.IO.Compression.FileSystem;" +
|
|
138
|
+
"[System.IO.Compression.ZipFile]::ExtractToDirectory($env:DELTA_CLI_ARCHIVE,$env:DELTA_CLI_DEST)";
|
|
139
|
+
execFileSync("powershell.exe", [...psOpts, dotnet], { stdio: ["ignore", "inherit", "inherit"], env: psEnv });
|
|
140
|
+
} catch (primaryErr) {
|
|
235
141
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
142
|
+
const cmdlet =
|
|
143
|
+
"$ErrorActionPreference='Stop';" +
|
|
144
|
+
"Expand-Archive -LiteralPath $env:DELTA_CLI_ARCHIVE -DestinationPath $env:DELTA_CLI_DEST -Force";
|
|
145
|
+
execFileSync("powershell.exe", [...psOpts, cmdlet], { stdio: ["ignore", "inherit", "inherit"], env: psEnv });
|
|
146
|
+
} catch (secondErr) {
|
|
147
|
+
try {
|
|
148
|
+
execFileSync("tar", ["-xf", archivePath, "-C", destDir], { stdio: "ignore" });
|
|
149
|
+
} catch (fallbackErr) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Failed to extract ${archivePath}. ` +
|
|
152
|
+
`.NET ZipFile: ${primaryErr.message}. ` +
|
|
153
|
+
`Expand-Archive: ${secondErr.message}. ` +
|
|
154
|
+
`tar: ${fallbackErr.message}`
|
|
155
|
+
);
|
|
240
156
|
}
|
|
241
|
-
console.warn(`[delta-cli] curl download failed (${err.message}), falling back to Node https.`);
|
|
242
157
|
}
|
|
243
158
|
}
|
|
244
|
-
|
|
245
|
-
return nodeDownload(url, dest);
|
|
246
159
|
}
|
|
247
160
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
161
|
+
// ── Checksum ───────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function getExpectedChecksum(archiveName, checksumsDir) {
|
|
164
|
+
const dir = checksumsDir || path.join(__dirname, "..");
|
|
165
|
+
const checksumsPath = path.join(dir, "checksums.txt");
|
|
166
|
+
if (!fs.existsSync(checksumsPath)) {
|
|
167
|
+
console.error("[WARN] checksums.txt not found, skipping checksum verification");
|
|
168
|
+
return null;
|
|
256
169
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
170
|
+
const content = fs.readFileSync(checksumsPath, "utf8");
|
|
171
|
+
for (const line of content.split("\n")) {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
if (!trimmed) continue;
|
|
174
|
+
const idx = trimmed.indexOf(" ");
|
|
175
|
+
if (idx === -1) continue;
|
|
176
|
+
const hash = trimmed.slice(0, idx);
|
|
177
|
+
const name = trimmed.slice(idx + 2);
|
|
178
|
+
if (name === archiveName) return hash;
|
|
260
179
|
}
|
|
261
|
-
|
|
180
|
+
throw new Error(`Checksum entry not found for ${archiveName}`);
|
|
262
181
|
}
|
|
263
182
|
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
183
|
+
function verifyChecksum(archivePath, expectedHash) {
|
|
184
|
+
if (expectedHash === null) return;
|
|
185
|
+
const hash = crypto.createHash("sha256");
|
|
186
|
+
const fd = fs.openSync(archivePath, "r");
|
|
187
|
+
try {
|
|
188
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
189
|
+
let bytesRead;
|
|
190
|
+
while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
191
|
+
hash.update(buf.subarray(0, bytesRead));
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
fs.closeSync(fd);
|
|
195
|
+
}
|
|
196
|
+
const actual = hash.digest("hex");
|
|
197
|
+
if (actual.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`[SECURITY] Checksum mismatch for ${path.basename(archivePath)}: expected ${expectedHash} but got ${actual}`
|
|
272
200
|
);
|
|
273
|
-
} else {
|
|
274
|
-
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' });
|
|
275
201
|
}
|
|
276
202
|
}
|
|
277
203
|
|
|
278
|
-
|
|
279
|
-
const fatal = options.fatal === true;
|
|
280
|
-
const allowSkip = options.allowSkip !== false;
|
|
204
|
+
// ── Main install ───────────────────────────────────────────────────────────
|
|
281
205
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const platform = platformMap[process.platform];
|
|
288
|
-
const arch = archMap[process.arch];
|
|
289
|
-
if (!platform || !arch) {
|
|
290
|
-
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const isWin = platform === 'windows';
|
|
294
|
-
const archiveExt = isWin ? '.zip' : '.tar.gz';
|
|
295
|
-
const binaryExt = isWin ? '.exe' : '';
|
|
296
|
-
const assetName = `${BINARY_NAME}-${platform}-${arch}${archiveExt}`;
|
|
297
|
-
const binDir = path.join(__dirname, '..', 'bin');
|
|
298
|
-
const archivePath = path.join(binDir, assetName);
|
|
299
|
-
const binaryPath = path.join(binDir, `${BINARY_NAME}${binaryExt}`);
|
|
206
|
+
function install() {
|
|
207
|
+
const downloadUrls = releaseAssetUrls(VERSION, archiveName);
|
|
300
208
|
|
|
301
209
|
fs.mkdirSync(binDir, { recursive: true });
|
|
210
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "delta-cli-"));
|
|
211
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
302
212
|
|
|
303
|
-
|
|
304
|
-
if (localArchive) {
|
|
305
|
-
if (!fs.existsSync(localArchive)) {
|
|
306
|
-
throw new Error(`DELTA_CLI_ARCHIVE file not found: ${localArchive}`);
|
|
307
|
-
}
|
|
308
|
-
fs.copyFileSync(localArchive, archivePath);
|
|
309
|
-
} else {
|
|
310
|
-
const urls = releaseAssetUrls(VERSION, assetName);
|
|
213
|
+
try {
|
|
311
214
|
let lastErr;
|
|
312
|
-
let
|
|
313
|
-
for (const url of
|
|
314
|
-
console.log(`[delta-cli] Downloading ${assetName} from ${new URL(url).hostname}...`);
|
|
215
|
+
let downloaded = false;
|
|
216
|
+
for (const url of downloadUrls) {
|
|
315
217
|
try {
|
|
316
|
-
|
|
218
|
+
download(url, archivePath);
|
|
219
|
+
downloaded = true;
|
|
317
220
|
break;
|
|
318
|
-
} catch (
|
|
319
|
-
|
|
320
|
-
lastErr = err;
|
|
221
|
+
} catch (e) {
|
|
222
|
+
lastErr = e;
|
|
321
223
|
}
|
|
322
224
|
}
|
|
323
|
-
if (
|
|
324
|
-
throw lastErr || new Error('All download sources failed');
|
|
325
|
-
}
|
|
326
|
-
}
|
|
225
|
+
if (!downloaded) throw lastErr;
|
|
327
226
|
|
|
328
|
-
|
|
329
|
-
|
|
227
|
+
const expectedHash = getExpectedChecksum(archiveName);
|
|
228
|
+
verifyChecksum(archivePath, expectedHash);
|
|
330
229
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
230
|
+
if (isWindows) {
|
|
231
|
+
extractZipWindows(archivePath, tmpDir);
|
|
232
|
+
} else {
|
|
233
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], { stdio: "ignore" });
|
|
234
|
+
}
|
|
334
235
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
fs.
|
|
236
|
+
const binaryName = NAME + (isWindows ? ".exe" : "");
|
|
237
|
+
const extractedBinary = path.join(tmpDir, `${NAME}-${platform}-${arch}`);
|
|
238
|
+
if (fs.existsSync(extractedBinary)) {
|
|
239
|
+
fs.copyFileSync(extractedBinary, dest);
|
|
240
|
+
} else {
|
|
241
|
+
// Fallback: try the plain name (older release format)
|
|
242
|
+
const plainBinary = path.join(tmpDir, binaryName);
|
|
243
|
+
if (fs.existsSync(plainBinary)) {
|
|
244
|
+
fs.copyFileSync(plainBinary, dest);
|
|
245
|
+
} else {
|
|
246
|
+
// Search in tmpDir
|
|
247
|
+
const files = fs.readdirSync(tmpDir).filter(f => f.includes(NAME) || f.includes(binaryName));
|
|
248
|
+
if (files.length > 0) {
|
|
249
|
+
fs.copyFileSync(path.join(tmpDir, files[0]), dest);
|
|
250
|
+
} else {
|
|
251
|
+
throw new Error(`Binary not found in extracted archive. Files: ${fs.readdirSync(tmpDir).join(", ")}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
fs.chmodSync(dest, 0o755);
|
|
256
|
+
} finally {
|
|
257
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
338
258
|
}
|
|
259
|
+
}
|
|
339
260
|
|
|
340
|
-
|
|
341
|
-
throw new Error(`Binary not found after extraction: ${binaryPath}`);
|
|
342
|
-
}
|
|
261
|
+
// ── Entry ──────────────────────────────────────────────────────────────────
|
|
343
262
|
|
|
344
|
-
|
|
345
|
-
|
|
263
|
+
if (require.main === module) {
|
|
264
|
+
if (!platform || !arch) {
|
|
265
|
+
console.error(`Unsupported platform: ${process.platform}-${process.arch}`);
|
|
266
|
+
process.exit(1);
|
|
346
267
|
}
|
|
347
268
|
|
|
348
|
-
|
|
349
|
-
|
|
269
|
+
// npx postinstall guard: skip binary download in ephemeral npx context
|
|
270
|
+
const isNpxPostinstall =
|
|
271
|
+
process.env.npm_command === "exec" && !process.env.DELTA_CLI_RUN;
|
|
350
272
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const message = err && err.message ? err.message : String(err);
|
|
354
|
-
if (fatal) {
|
|
355
|
-
console.error('[delta-cli] install failed:', message);
|
|
356
|
-
process.exit(1);
|
|
273
|
+
if (isNpxPostinstall) {
|
|
274
|
+
process.exit(0);
|
|
357
275
|
}
|
|
358
|
-
console.error('[delta-cli] postinstall warning:', message);
|
|
359
|
-
console.error('The wrapper is installed, but the native binary may be missing.');
|
|
360
|
-
console.error('Run "delta-cli" once to retry, or set DELTA_CLI_ARCHIVE to a local archive path.');
|
|
361
|
-
process.exit(0);
|
|
362
|
-
}
|
|
363
276
|
|
|
364
|
-
|
|
365
|
-
|
|
277
|
+
try {
|
|
278
|
+
install();
|
|
279
|
+
console.log(`${NAME} v${VERSION} installed successfully`);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error(`Failed to install ${NAME}:`, err.message);
|
|
282
|
+
console.error(
|
|
283
|
+
`\nIf you are behind a firewall or in a restricted network, try one of:\n` +
|
|
284
|
+
` # 1. Use a proxy:\n` +
|
|
285
|
+
` export https_proxy=http://your-proxy:port\n` +
|
|
286
|
+
` npm install -g @delta-infra/cli\n\n` +
|
|
287
|
+
` # 2. Point to a corporate npm mirror:\n` +
|
|
288
|
+
` npm install -g @delta-infra/cli --registry=https://your-corp-mirror/\n` +
|
|
289
|
+
`\nOr manually download from:\n` +
|
|
290
|
+
` https://github.com/${REPO}/releases/tag/v${VERSION}`
|
|
291
|
+
);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
366
294
|
}
|
|
367
295
|
|
|
368
|
-
module.exports = {
|
|
296
|
+
module.exports = {
|
|
297
|
+
getExpectedChecksum, verifyChecksum, assertAllowedHost,
|
|
298
|
+
isCurlVersionSupported, curlSupportsSslRevokeBestEffort,
|
|
299
|
+
};
|
package/scripts/run.js
CHANGED
|
@@ -1,60 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
const { install } = require('./install');
|
|
2
|
+
// Copyright (c) 2026 Delta Infra Authors
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
6
4
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'bin',
|
|
11
|
-
process.platform === 'win32' ? 'delta-cli.exe' : 'delta-cli'
|
|
12
|
-
);
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
console.error('[delta-cli] Binary not found, attempting to install...');
|
|
19
|
-
await install({ allowSkip: false, fatal: true });
|
|
20
|
-
if (!fs.existsSync(binary)) {
|
|
21
|
-
throw new Error('Binary is still missing after install attempt.');
|
|
22
|
-
}
|
|
23
|
-
}
|
|
9
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
10
|
+
const bin = path.join(__dirname, "..", "bin", "delta-cli" + ext);
|
|
24
11
|
|
|
25
|
-
|
|
12
|
+
// On Windows, a crashed self-update may have left the binary renamed to .old.
|
|
13
|
+
const oldBin = bin + ".old";
|
|
14
|
+
function restoreOldBinary() {
|
|
26
15
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
16
|
+
if (fs.existsSync(bin)) {
|
|
17
|
+
fs.rmSync(bin, { force: true });
|
|
18
|
+
}
|
|
19
|
+
fs.renameSync(oldBin, bin);
|
|
20
|
+
return true;
|
|
21
|
+
} catch { return false; }
|
|
22
|
+
}
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
24
|
+
if (process.platform === "win32" && fs.existsSync(oldBin)) {
|
|
25
|
+
if (!fs.existsSync(bin)) {
|
|
26
|
+
restoreOldBinary();
|
|
27
|
+
} else {
|
|
28
|
+
try {
|
|
29
|
+
execFileSync(bin, ["--version"], { stdio: "ignore", timeout: 10000 });
|
|
30
|
+
try { fs.rmSync(oldBin, { force: true }); } catch {}
|
|
31
|
+
} catch {
|
|
32
|
+
restoreOldBinary();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
console.error('[delta-cli] Failed to start:', err.message);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
});
|
|
37
|
+
const args = process.argv.slice(2);
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
// Intercept install/uninstall — run wizard, bypass native binary
|
|
40
|
+
if (args[0] === "install" || args[0] === "uninstall") {
|
|
41
|
+
require("./install-wizard.js");
|
|
42
|
+
} else {
|
|
43
|
+
// Auto-download binary if missing (e.g. npx skipped postinstall)
|
|
44
|
+
if (!fs.existsSync(bin)) {
|
|
45
|
+
try {
|
|
46
|
+
execFileSync(process.execPath, [path.join(__dirname, "install.js")], {
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
env: { ...process.env, DELTA_CLI_RUN: "true" },
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
console.error(
|
|
52
|
+
`\nFailed to auto-install delta-cli binary.\n` +
|
|
53
|
+
`To fix, run the install script manually:\n` +
|
|
54
|
+
` node "${path.join(__dirname, "install.js")}"\n`
|
|
55
|
+
);
|
|
55
56
|
process.exit(1);
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
try {
|
|
61
|
+
execFileSync(bin, args, { stdio: "inherit" });
|
|
62
|
+
} catch (e) {
|
|
63
|
+
process.exit(e.status || 1);
|
|
64
|
+
}
|
|
65
|
+
}
|