@delta-infra/cli 1.0.27 → 1.0.29

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/checksums.txt CHANGED
@@ -1,5 +1,5 @@
1
- 5e91055b7ece427c10b3110250deba8ae5e36558179c7d5bc8b40ad33756848a delta-cli-darwin-amd64.tar.gz
2
- 714633afc4617b421edf796a36aa458c28dea8dde73ad5b72a78fae82594ad25 delta-cli-darwin-arm64.tar.gz
3
- 8c60b6eab2f3d7737cce12392bc8f5a976d2e03e45d810537afe962fa6f8e6bd delta-cli-linux-amd64.tar.gz
4
- 773efdfbe622e2ef72900af61aaaf3c394ac55e4fcb06e430545db27e0a84f97 delta-cli-linux-arm64.tar.gz
5
- a2746bae82332f8fe330644f39edda2ec1702c847b75e255d01630782df9388c delta-cli-windows-amd64.zip
1
+ 82da0807522c3068c884fa45d66db62d959626b6441e353205c01b7300e140d1 delta-cli-darwin-amd64.tar.gz
2
+ 201cfa4569414aac3425bfd63aed7b787fb45d7f0c61bd6a89b4cc6ca34fb257 delta-cli-darwin-arm64.tar.gz
3
+ f34625f968474c61f5c30fbe76db61b12660ee4e42ed65992d961de0608c19b9 delta-cli-linux-amd64.tar.gz
4
+ db07f3b7fe47af0387cbaf984c000a09fc50920f506c969ea9c4bc07b7d90be8 delta-cli-linux-arm64.tar.gz
5
+ f50337759b256f7a87ac4f5a0a302f5c0fe947e934442c5ed2cf12890020476f 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.27",
3
+ "version": "1.0.29",
4
4
  "description": "Delta Sandbox CLI",
5
5
  "bin": {
6
6
  "delta-cli": "scripts/run.js"
@@ -41,5 +41,8 @@
41
41
  "repository": {
42
42
  "type": "git",
43
43
  "url": "git+https://github.com/yzailab/delta-infra-cli.git"
44
+ },
45
+ "dependencies": {
46
+ "@clack/prompts": "^1.2.0"
44
47
  }
45
48
  }
@@ -1,159 +1,412 @@
1
1
  #!/usr/bin/env node
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { execFileSync } = require('child_process');
5
- const readline = require('readline');
2
+ // Copyright (c) 2026 Delta Infra Authors
3
+ // SPDX-License-Identifier: MIT
6
4
 
7
- const PKG = '@delta-infra/cli';
8
- const SKILL_NAMES = ['delta-sandbox', 'delta-shared'];
9
- const CONFIG_DIR = path.join(osHomedir(), '.delta-infra');
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
+ }
10
118
 
11
119
  function osHomedir() {
12
120
  return process.env.HOME ||
13
121
  process.env.USERPROFILE ||
14
122
  (process.env.HOMEDRIVE && process.env.HOMEPATH && process.env.HOMEDRIVE + process.env.HOMEPATH) ||
15
- '/root';
123
+ "/root";
16
124
  }
17
125
 
18
- function log(msg) { console.log('[delta-cli]', msg); }
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
+ }
19
144
 
20
- function run(cmd, args, opts) {
21
- execFileSync(cmd, args, { stdio: 'inherit', timeout: 120000, ...opts });
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; }
22
151
  }
23
152
 
24
- function runSilent(cmd, args) {
25
- return execFileSync(cmd, args, { stdio: 'pipe', timeout: 30000 }).toString().trim();
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;
26
161
  }
27
162
 
28
- function isInstalledGlobally() {
163
+ function getGloballyInstalledVersion() {
29
164
  try {
30
- const out = runSilent('npm', ['list', '-g', PKG, '--depth=0']);
31
- return out.includes(PKG);
32
- } catch { return false; }
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; }
33
169
  }
34
170
 
35
- function promptYesNo(question) {
36
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
- return new Promise((resolve) => {
38
- rl.question(`${question} [y/N] `, (answer) => {
39
- rl.close();
40
- resolve(answer.trim().toLowerCase() === 'y');
41
- });
42
- });
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; }
43
177
  }
44
178
 
45
- async function stepInstallGlobally() {
46
- if (isInstalledGlobally()) {
47
- log(`${PKG} is already installed globally.`);
48
- return;
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;
49
189
  }
50
- log('Installing', PKG, 'globally...');
51
- run('npm', ['install', '-g', PKG]);
52
- log('Global install complete.');
53
- }
54
190
 
55
- async function stepInstallSkills() {
56
- log('Installing AI skills for delta-sandbox...');
191
+ const targetVer = latestVer || installedVer;
192
+ const s = p.spinner();
193
+ if (needsUpgrade) {
194
+ s.start(fmt(msg.upgrade, PKG, installedVer, latestVer));
195
+ } else {
196
+ s.start(fmt(msg.step1, PKG));
197
+ }
57
198
  try {
58
- run('npx', ['-y', 'skills', 'add', 'yzailab/delta-infra-cli', '-y', '-g']);
59
- log('AI skills installed.');
199
+ // Install the npm package (which triggers postinstall → install.js),
200
+ // then explicitly download the correct version binary in case the
201
+ // npx cache has stale package.json that misleads install.js.
202
+ await runSilentAsync("npm", ["install", "-g", PKG], { timeout: 120000 });
203
+ if (targetVer) {
204
+ await runSilentAsync(process.execPath, [
205
+ path.join(__dirname, "install.js"),
206
+ ], {
207
+ timeout: 120000,
208
+ env: { ...process.env, DELTA_CLI_RUN: "true", DELTA_CLI_VERSION: targetVer },
209
+ });
210
+ }
211
+ s.stop(needsUpgrade ? fmt(msg.step1Upgraded, latestVer) : msg.step1Done);
212
+ return needsUpgrade;
60
213
  } catch {
61
- log('Warning: skill install failed. You can retry:');
62
- log(' npx skills add yzailab/delta-infra-cli -y -g');
214
+ s.stop(fmt(msg.step1Fail, PKG));
215
+ process.exit(1);
63
216
  }
64
217
  }
65
218
 
66
- function stepConfigInit() {
67
- log('Run the following to initialize configuration:');
68
- log(' delta-cli config init');
219
+ async function skillsAlreadyInstalled() {
220
+ try {
221
+ const out = await runSilentAsync("npx", ["-y", "skills", "ls", "-g"], {
222
+ timeout: 120000,
223
+ });
224
+ return /^delta-/m.test(out.toString());
225
+ } catch { return false; }
69
226
  }
70
227
 
71
- function stepAuthLogin() {
72
- log('Run the following to authenticate:');
73
- log(' delta-cli auth login');
228
+ async function stepInstallSkills(msg) {
229
+ const s = p.spinner();
230
+ s.start(msg.step2Spinner);
231
+ try {
232
+ if (await skillsAlreadyInstalled()) {
233
+ s.stop(msg.step2Skip);
234
+ return;
235
+ }
236
+ try {
237
+ await runSilentAsync("npx", ["-y", "skills", "add", SKILLS_REPO, "-y", "-g"], {
238
+ timeout: 120000,
239
+ });
240
+ } catch {
241
+ // Retry with GitHub URL directly
242
+ await runSilentAsync("npx", [
243
+ "-y", "skills", "add",
244
+ `https://github.com/${SKILLS_REPO}`, "-y", "-g",
245
+ ], { timeout: 120000 });
246
+ }
247
+ s.stop(msg.step2Done);
248
+ } catch {
249
+ s.stop(fmt(msg.step2Fail, SKILLS_REPO));
250
+ process.exit(1);
251
+ }
74
252
  }
75
253
 
76
- async function stepUninstallPackage() {
77
- if (!isInstalledGlobally()) {
78
- log(`${PKG} is not installed globally.`);
254
+ async function stepConfigInit(msg) {
255
+ const s = p.spinner();
256
+ s.start(msg.step3);
257
+
258
+ const deltaCli = whichDeltaCli();
259
+ if (!deltaCli) {
260
+ s.stop(msg.step3);
261
+ p.log.warn("delta-cli not found on PATH after global install.");
262
+ p.log.info(msg.step3Fail);
79
263
  return;
80
264
  }
81
- log('Removing', PKG, 'globally...');
82
- run('npm', ['uninstall', '-g', PKG]);
83
- log('Package removed.');
265
+
266
+ // Check if config already exists with valid data
267
+ const existingConfig = getExistingConfig();
268
+ s.stop(msg.step3);
269
+
270
+ if (existingConfig && existingConfig.base_url) {
271
+ const reuse = await p.confirm({
272
+ message: `发现已有配置 (Server: ${existingConfig.base_url}),继续使用?`,
273
+ });
274
+ if (handleCancel(reuse, msg) && reuse) {
275
+ p.log.info(msg.step3Skip);
276
+ return;
277
+ }
278
+ }
279
+
280
+ try {
281
+ run(deltaCli, ["config", "init"]);
282
+ p.log.success(msg.step3Done);
283
+ } catch {
284
+ p.log.error(msg.step3Fail);
285
+ process.exit(1);
286
+ }
84
287
  }
85
288
 
86
- async function stepUninstallSkills() {
87
- log('Removing AI skills...');
289
+ async function stepAuthLogin(msg) {
290
+ const deltaCli = whichDeltaCli();
291
+ if (!deltaCli) {
292
+ p.log.warn("delta-cli not found, skipping authorization");
293
+ return;
294
+ }
295
+
296
+ const yes = await p.confirm({
297
+ message: msg.step4Confirm,
298
+ });
299
+ if (p.isCancel(yes)) {
300
+ p.cancel(msg.cancelled);
301
+ process.exit(0);
302
+ }
303
+ if (!yes) {
304
+ p.log.info(msg.step4Skip);
305
+ return;
306
+ }
307
+
308
+ p.log.step(msg.step4);
88
309
  try {
89
- run('npx', ['-y', 'skills', 'remove', ...SKILL_NAMES, '-g']);
90
- log('AI skills removed.');
310
+ run(deltaCli, ["auth", "login"]);
311
+ p.log.success(msg.step4Done);
91
312
  } catch {
92
- log('Warning: skill removal failed. You can retry:');
93
- log(` npx skills remove ${SKILL_NAMES.join(' ')} -g`);
313
+ p.log.warn(msg.step4Fail);
314
+ }
315
+ }
316
+
317
+ // ── Uninstall steps ─────────────────────────────────────────────────────────
318
+
319
+ async function stepUninstallSkills(msg) {
320
+ const s = p.spinner();
321
+ s.start("Removing AI skills...");
322
+ try {
323
+ await runSilentAsync("npx", ["-y", "skills", "remove", "delta-sandbox", "delta-shared", "-g"], {
324
+ timeout: 120000,
325
+ });
326
+ s.stop("AI skills removed");
327
+ } catch {
328
+ s.stop("Warning: skill removal failed. You can retry:");
329
+ p.log.info(" npx skills remove delta-sandbox delta-shared -g");
94
330
  }
95
331
  }
96
332
 
97
333
  function stepRemoveConfig() {
98
334
  if (!fs.existsSync(CONFIG_DIR)) {
99
- log('No config directory found.');
100
335
  return;
101
336
  }
102
- log('Removing config directory:', CONFIG_DIR);
103
337
  fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
104
- log('Config removed.');
105
338
  }
106
339
 
107
- async function doInstall() {
108
- const isInteractive = !!process.stdin.isTTY;
340
+ async function stepUninstallPackage(msg) {
341
+ const installedVer = getGloballyInstalledVersion();
342
+ if (!installedVer) {
343
+ return;
344
+ }
345
+ const s = p.spinner();
346
+ s.start("Removing global package...");
347
+ try {
348
+ await runSilentAsync("npm", ["uninstall", "-g", PKG], { timeout: 60000 });
349
+ s.stop("Package removed");
350
+ } catch {
351
+ s.stop("Failed to remove global package");
352
+ p.log.info(` npm uninstall -g ${PKG}`);
353
+ }
354
+ }
109
355
 
110
- log('Setting up Delta Sandbox CLI...\n');
356
+ // ── Main ────────────────────────────────────────────────────────────────────
111
357
 
112
- stepInstallGlobally();
113
- stepInstallSkills();
358
+ async function doInstall() {
359
+ const isInteractive = !!process.stdin.isTTY;
114
360
 
115
361
  if (isInteractive) {
116
- stepConfigInit();
117
- stepAuthLogin();
118
- log('\nSetup complete! You can now use delta-cli.');
362
+ p.intro(messages.zh.setup);
363
+ await stepInstallGlobally(messages.zh);
364
+ await stepInstallSkills(messages.zh);
365
+ await stepConfigInit(messages.zh);
366
+ await stepAuthLogin(messages.zh);
367
+ p.outro(messages.zh.done);
119
368
  } else {
120
- log('\nNon-interactive mode: config and auth skipped.');
121
- log('To complete setup, manually run:');
122
- log(' delta-cli config init');
123
- log(' delta-cli auth login');
369
+ console.log(messages.zh.setup);
370
+ await stepInstallGlobally(messages.zh);
371
+ await stepInstallSkills(messages.zh);
372
+ console.log(messages.zh.nonTtyHint);
124
373
  }
125
374
  }
126
375
 
127
376
  async function doUninstall() {
128
377
  const isInteractive = !!process.stdin.isTTY;
129
378
 
130
- log('Uninstalling Delta Sandbox CLI...\n');
131
-
132
379
  if (isInteractive) {
133
- const ok = await promptYesNo('This will remove the global package, AI skills, and config. Continue?');
134
- if (!ok) {
135
- log('Uninstall cancelled.');
380
+ const ok = await p.confirm({
381
+ message: "将移除全局包、AI Skills 和配置文件。是否继续?",
382
+ });
383
+ if (p.isCancel(ok) || !ok) {
384
+ p.cancel("卸载已取消");
136
385
  process.exit(0);
137
386
  }
138
387
  }
139
388
 
140
- stepRemoveConfig();
141
- stepUninstallSkills();
142
- stepUninstallPackage();
389
+ await stepRemoveConfig();
390
+ await stepUninstallSkills();
391
+ await stepUninstallPackage();
143
392
 
144
- log('\nUninstall complete.');
393
+ if (isInteractive) {
394
+ p.outro("卸载完成");
395
+ } else {
396
+ console.log("\nUninstall complete.");
397
+ }
145
398
  }
146
399
 
147
400
  function main() {
148
401
  const subcommand = process.argv[2];
149
- if (subcommand === 'uninstall') {
402
+ if (subcommand === "uninstall") {
150
403
  doUninstall().catch((e) => {
151
- console.error('[delta-cli] Uninstall failed:', e.message);
404
+ console.error("[delta-cli] Uninstall failed:", e.message);
152
405
  process.exit(1);
153
406
  });
154
407
  } else {
155
408
  doInstall().catch((e) => {
156
- console.error('[delta-cli] Install failed:', e.message);
409
+ console.error("[delta-cli] Install failed:", e.message);
157
410
  process.exit(1);
158
411
  });
159
412
  }
@@ -1,374 +1,317 @@
1
- #!/usr/bin/env node
2
- const fs = require('fs');
3
- const path = require('path');
4
- const https = require('https');
5
- const crypto = require('crypto');
6
- const { execSync, spawn } = require('child_process');
7
-
8
- const BINARY_NAME = 'delta-cli';
9
- const VERSION = require('../package.json').version;
10
-
11
- const DEFAULT_ALLOWED_HOSTS = [
12
- 'github.com',
13
- 'gh-proxy.com',
14
- 'objects.githubusercontent.com',
15
- 'release-assets.githubusercontent.com',
16
- 'github-releases.githubusercontent.com',
17
- 'raw.githubusercontent.com',
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 PKG = "@delta-infra/cli";
11
+
12
+ // Resolve the version to download:
13
+ // env var > npm registry > local package.json (fallback)
14
+ function resolveVersion() {
15
+ if (process.env.DELTA_CLI_VERSION) return process.env.DELTA_CLI_VERSION.replace(/^v/, "");
16
+ try {
17
+ const out = require("child_process").execFileSync("npm", ["view", PKG, "version"], {
18
+ stdio: ["ignore", "pipe", "pipe"],
19
+ timeout: 15000,
20
+ encoding: "utf8",
21
+ });
22
+ const ver = out.trim();
23
+ if (/^\d+\.\d+\.\d+/.test(ver)) return ver;
24
+ } catch {}
25
+ return require("../package.json").version.replace(/-.*$/, "");
26
+ }
27
+
28
+ const VERSION = resolveVersion();
29
+ const NAME = "delta-cli";
30
+ const REPO = "yzailab/delta-infra-cli";
31
+ const DEFAULT_ALLOWED_HOSTS = new Set([
32
+ "github.com",
33
+ "gh-proxy.com",
34
+ "objects.githubusercontent.com",
35
+ "release-assets.githubusercontent.com",
36
+ "github-releases.githubusercontent.com",
37
+ "raw.githubusercontent.com",
38
+ "registry.npmmirror.com",
39
+ ]);
19
40
  const DEFAULT_ALLOWED_HOST_PREFIXES = [
20
- 'github-production-release-asset-',
41
+ "github-production-release-asset-",
21
42
  ];
22
- const MAX_REDIRECTS = 5;
23
43
 
24
- const platformMap = {
25
- darwin: 'darwin',
26
- linux: 'linux',
27
- win32: 'windows',
44
+ const PLATFORM_MAP = {
45
+ darwin: "darwin",
46
+ linux: "linux",
47
+ win32: "windows",
28
48
  };
29
49
 
30
- const archMap = {
31
- x64: 'amd64',
32
- arm64: 'arm64',
50
+ const ARCH_MAP = {
51
+ x64: "amd64",
52
+ arm64: "arm64",
33
53
  };
34
54
 
35
- const RELEASE_SOURCE_BASE = 'https://github.com/yzailab/delta-infra-cli/releases/download';
36
- const RELEASE_DOMESTIC_BASE = 'https://gh-proxy.com/https://github.com/yzailab/delta-infra-cli/releases/download';
55
+ const platform = PLATFORM_MAP[process.platform];
56
+ const arch = ARCH_MAP[process.arch];
57
+
58
+ const isWindows = process.platform === "win32";
59
+ const ext = isWindows ? ".zip" : ".tar.gz";
60
+ const archiveName = `${NAME}-${platform}-${arch}${ext}`;
61
+ const GITHUB_URL = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
62
+
63
+ const binDir = path.join(__dirname, "..", "bin");
64
+ const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
65
+
66
+ // ── Mirror URL resolution ──────────────────────────────────────────────────
37
67
 
38
68
  function releaseAssetUrls(version, assetName) {
39
69
  const urls = [];
40
- const mirror = process.env.DELTA_CLI_MIRROR || '';
70
+ const mirror = process.env.DELTA_CLI_MIRROR || "";
41
71
  if (mirror) {
42
- urls.push(`${mirror.replace(/\/$/, '')}/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`);
72
+ urls.push(`${mirror.replace(/\/$/, "")}/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`);
43
73
  }
44
- urls.push(`${RELEASE_DOMESTIC_BASE}/v${version}/${assetName}`);
45
- urls.push(`${RELEASE_SOURCE_BASE}/v${version}/${assetName}`);
74
+ // China acceleration mirror (gh-proxy.com)
75
+ urls.push(`https://gh-proxy.com/https://github.com/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`);
76
+ // GitHub source
77
+ urls.push(GITHUB_URL);
46
78
  return urls;
47
79
  }
48
80
 
49
- function resolveUrl(base, location) {
50
- if (!location) return base;
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();
81
+ function assertAllowedHost(url) {
82
+ const { hostname } = new URL(url);
105
83
  const allowlist = new Set(DEFAULT_ALLOWED_HOSTS);
106
- (process.env.DELTA_CLI_MIRROR_ALLOWLIST || '')
107
- .split(',')
84
+ (process.env.DELTA_CLI_MIRROR_ALLOWLIST || "")
85
+ .split(",")
108
86
  .map((h) => h.trim().toLowerCase())
109
87
  .filter(Boolean)
110
88
  .forEach((h) => allowlist.add(h));
111
89
  for (const allowed of allowlist) {
112
- if (host === allowed || host.endsWith('.' + allowed)) {
113
- return true;
114
- }
90
+ if (hostname === allowed || hostname.endsWith("." + allowed)) return;
115
91
  }
116
92
  for (const prefix of DEFAULT_ALLOWED_HOST_PREFIXES) {
117
- if (host.startsWith(prefix)) {
118
- return true;
119
- }
93
+ if (hostname.startsWith(prefix)) return;
120
94
  }
121
- return false;
95
+ throw new Error(`Download host not allowed: ${hostname}`);
122
96
  }
123
97
 
124
- function getHttpProxy() {
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
- }
98
+ // ── curl version detection (for --ssl-revoke-best-effort) ──────────────────
133
99
 
134
- const DOWNLOAD_TIMEOUT_MS = parseInt(process.env.DELTA_CLI_DOWNLOAD_TIMEOUT, 10) || 120000;
100
+ function isCurlVersionSupported(versionOutput) {
101
+ const match = String(versionOutput).match(/^\s*curl\s+(\d+)\.(\d+)\.(\d+)/i);
102
+ if (!match) return false;
103
+ const major = parseInt(match[1], 10);
104
+ const minor = parseInt(match[2], 10);
105
+ return major > 7 || (major === 7 && minor >= 70);
106
+ }
135
107
 
136
- function nodeDownload(url, dest, redirectCount = 0) {
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
- }
108
+ let _curlSupportsSslRevokeBestEffort;
146
109
 
147
- const file = fs.createWriteStream(dest);
148
- const hash = crypto.createHash('sha256');
149
- let finished = false;
150
- const maxTimer = setTimeout(() => {
151
- cleanup(new Error(`Download did not complete within ${DOWNLOAD_TIMEOUT_MS}ms`));
152
- }, DOWNLOAD_TIMEOUT_MS);
153
-
154
- const cleanup = (err) => {
155
- if (finished) return;
156
- finished = true;
157
- clearTimeout(maxTimer);
158
- try {
159
- file.destroy();
160
- fs.unlinkSync(dest);
161
- } catch {}
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
- });
110
+ function curlSupportsSslRevokeBestEffort() {
111
+ if (_curlSupportsSslRevokeBestEffort !== undefined) {
112
+ return _curlSupportsSslRevokeBestEffort;
113
+ }
114
+ try {
115
+ const output = execFileSync("curl", ["--version"], {
116
+ stdio: ["ignore", "pipe", "ignore"],
117
+ encoding: "utf8",
118
+ timeout: 5000,
119
+ });
120
+ _curlSupportsSslRevokeBestEffort = isCurlVersionSupported(output);
121
+ } catch {
122
+ _curlSupportsSslRevokeBestEffort = false;
123
+ }
124
+ return _curlSupportsSslRevokeBestEffort;
205
125
  }
206
126
 
207
- function curlDownload(url, dest, proxy) {
208
- return new Promise((resolve, reject) => {
209
- const maxSeconds = Math.ceil(DOWNLOAD_TIMEOUT_MS / 1000);
210
- const args = ['-L', '--fail', '--show-error', '--silent', '--max-time', String(maxSeconds), '-o', dest];
211
- if (proxy) {
212
- args.push('--proxy', proxy);
213
- }
214
- args.push(url);
215
- const child = spawn('curl', args, { stdio: ['ignore', 'inherit', 'inherit'] });
216
- child.on('error', (err) => {
217
- reject(new Error(`curl not available or failed to start: ${err.message}`));
218
- });
219
- child.on('close', (code) => {
220
- if (code !== 0) {
221
- reject(new Error(`curl download failed (exit ${code})`));
222
- return;
223
- }
224
- fileSha256(dest).then(resolve).catch(reject);
225
- });
226
- });
127
+ // ── Download ───────────────────────────────────────────────────────────────
128
+
129
+ function download(url, destPath) {
130
+ assertAllowedHost(url);
131
+ const args = [
132
+ "--fail", "--location", "--silent", "--show-error",
133
+ "--connect-timeout", "10", "--max-time", "120",
134
+ "--max-redirs", "3",
135
+ "--output", destPath,
136
+ ];
137
+ if (isWindows && curlSupportsSslRevokeBestEffort()) {
138
+ args.unshift("--ssl-revoke-best-effort");
139
+ }
140
+ args.push(url);
141
+ execFileSync("curl", args, { stdio: ["ignore", "ignore", "pipe"] });
227
142
  }
228
143
 
229
- async function download(url, dest) {
230
- const proxy = getHttpProxy();
231
- const preferCurl =
232
- process.env.DELTA_CLI_USE_CURL === '1' || (!!proxy && process.env.DELTA_CLI_USE_CURL !== '0');
144
+ // ── Extraction ─────────────────────────────────────────────────────────────
233
145
 
234
- if (preferCurl) {
146
+ function extractZipWindows(archivePath, destDir) {
147
+ const psOpts = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"];
148
+ const psEnv = {
149
+ DELTA_CLI_ARCHIVE: archivePath,
150
+ DELTA_CLI_DEST: destDir,
151
+ };
152
+ try {
153
+ const dotnet =
154
+ "$ErrorActionPreference='Stop';" +
155
+ "Add-Type -AssemblyName System.IO.Compression.FileSystem;" +
156
+ "[System.IO.Compression.ZipFile]::ExtractToDirectory($env:DELTA_CLI_ARCHIVE,$env:DELTA_CLI_DEST)";
157
+ execFileSync("powershell.exe", [...psOpts, dotnet], { stdio: ["ignore", "inherit", "inherit"], env: psEnv });
158
+ } catch (primaryErr) {
235
159
  try {
236
- return await curlDownload(url, dest, proxy);
237
- } catch (err) {
238
- if (process.env.DELTA_CLI_USE_CURL === '1') {
239
- throw err;
160
+ const cmdlet =
161
+ "$ErrorActionPreference='Stop';" +
162
+ "Expand-Archive -LiteralPath $env:DELTA_CLI_ARCHIVE -DestinationPath $env:DELTA_CLI_DEST -Force";
163
+ execFileSync("powershell.exe", [...psOpts, cmdlet], { stdio: ["ignore", "inherit", "inherit"], env: psEnv });
164
+ } catch (secondErr) {
165
+ try {
166
+ execFileSync("tar", ["-xf", archivePath, "-C", destDir], { stdio: "ignore" });
167
+ } catch (fallbackErr) {
168
+ throw new Error(
169
+ `Failed to extract ${archivePath}. ` +
170
+ `.NET ZipFile: ${primaryErr.message}. ` +
171
+ `Expand-Archive: ${secondErr.message}. ` +
172
+ `tar: ${fallbackErr.message}`
173
+ );
240
174
  }
241
- console.warn(`[delta-cli] curl download failed (${err.message}), falling back to Node https.`);
242
175
  }
243
176
  }
244
-
245
- return nodeDownload(url, dest);
246
177
  }
247
178
 
248
- function verifyAsset(assetName, filePath, checksums) {
249
- if (!checksums) {
250
- console.warn('[delta-cli] checksums.txt not found; skipping checksum verification.');
251
- return;
252
- }
253
- const expected = checksums.get(assetName);
254
- if (!expected) {
255
- throw new Error(`No checksum found for ${assetName} in checksums.txt`);
179
+ // ── Checksum ───────────────────────────────────────────────────────────────
180
+
181
+ function getExpectedChecksum(archiveName, checksumsDir) {
182
+ const dir = checksumsDir || path.join(__dirname, "..");
183
+ const checksumsPath = path.join(dir, "checksums.txt");
184
+ if (!fs.existsSync(checksumsPath)) {
185
+ console.error("[WARN] checksums.txt not found, skipping checksum verification");
186
+ return null;
256
187
  }
257
- const actual = crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
258
- if (actual !== expected) {
259
- throw new Error(`Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`);
188
+ const content = fs.readFileSync(checksumsPath, "utf8");
189
+ for (const line of content.split("\n")) {
190
+ const trimmed = line.trim();
191
+ if (!trimmed) continue;
192
+ const idx = trimmed.indexOf(" ");
193
+ if (idx === -1) continue;
194
+ const hash = trimmed.slice(0, idx);
195
+ const name = trimmed.slice(idx + 2);
196
+ if (name === archiveName) return hash;
260
197
  }
261
- console.log(`[delta-cli] Checksum OK for ${assetName}.`);
198
+ throw new Error(`Checksum entry not found for ${archiveName}`);
262
199
  }
263
200
 
264
- function extract(archivePath, destDir, platform) {
265
- fs.mkdirSync(destDir, { recursive: true });
266
- if (platform === 'windows') {
267
- const escapedArchive = archivePath.replace(/'/g, "''");
268
- const escapedDest = destDir.replace(/'/g, "''");
269
- execSync(
270
- `powershell.exe -NoProfile -Command "Expand-Archive -Path '${escapedArchive}' -DestinationPath '${escapedDest}' -Force"`,
271
- { stdio: 'inherit' }
201
+ function verifyChecksum(archivePath, expectedHash) {
202
+ if (expectedHash === null) return;
203
+ const hash = crypto.createHash("sha256");
204
+ const fd = fs.openSync(archivePath, "r");
205
+ try {
206
+ const buf = Buffer.alloc(64 * 1024);
207
+ let bytesRead;
208
+ while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
209
+ hash.update(buf.subarray(0, bytesRead));
210
+ }
211
+ } finally {
212
+ fs.closeSync(fd);
213
+ }
214
+ const actual = hash.digest("hex");
215
+ if (actual.toLowerCase() !== expectedHash.toLowerCase()) {
216
+ throw new Error(
217
+ `[SECURITY] Checksum mismatch for ${path.basename(archivePath)}: expected ${expectedHash} but got ${actual}`
272
218
  );
273
- } else {
274
- execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' });
275
219
  }
276
220
  }
277
221
 
278
- async function install(options = {}) {
279
- const fatal = options.fatal === true;
280
- const allowSkip = options.allowSkip !== false;
281
-
282
- if (allowSkip && (process.env.CI === 'true' || process.env.CI === '1' || process.env.DELTA_CLI_SKIP_POSTINSTALL)) {
283
- console.log('[delta-cli] postinstall skipped (CI or DELTA_CLI_SKIP_POSTINSTALL).');
284
- return;
285
- }
222
+ // ── Main install ───────────────────────────────────────────────────────────
286
223
 
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}`);
224
+ function install() {
225
+ const downloadUrls = releaseAssetUrls(VERSION, archiveName);
300
226
 
301
227
  fs.mkdirSync(binDir, { recursive: true });
228
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "delta-cli-"));
229
+ const archivePath = path.join(tmpDir, archiveName);
302
230
 
303
- const localArchive = process.env.DELTA_CLI_ARCHIVE;
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);
231
+ try {
311
232
  let lastErr;
312
- let checksum;
313
- for (const url of urls) {
314
- console.log(`[delta-cli] Downloading ${assetName} from ${new URL(url).hostname}...`);
233
+ let downloaded = false;
234
+ for (const url of downloadUrls) {
315
235
  try {
316
- checksum = await download(url, archivePath);
236
+ download(url, archivePath);
237
+ downloaded = true;
317
238
  break;
318
- } catch (err) {
319
- console.warn(`[delta-cli] Download failed from ${url}: ${err.message}`);
320
- lastErr = err;
239
+ } catch (e) {
240
+ lastErr = e;
321
241
  }
322
242
  }
323
- if (checksum === undefined) {
324
- throw lastErr || new Error('All download sources failed');
325
- }
326
- }
243
+ if (!downloaded) throw lastErr;
327
244
 
328
- const checksums = loadChecksums();
329
- verifyAsset(assetName, archivePath, checksums);
245
+ const expectedHash = getExpectedChecksum(archiveName);
246
+ verifyChecksum(archivePath, expectedHash);
330
247
 
331
- console.log(`[delta-cli] Extracting to ${binDir}...`);
332
- extract(archivePath, binDir, platform);
333
- fs.unlinkSync(archivePath);
248
+ if (isWindows) {
249
+ extractZipWindows(archivePath, tmpDir);
250
+ } else {
251
+ execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], { stdio: "ignore" });
252
+ }
334
253
 
335
- const extractedBinary = path.join(binDir, `${BINARY_NAME}-${platform}-${arch}${binaryExt}`);
336
- if (fs.existsSync(extractedBinary) && extractedBinary !== binaryPath) {
337
- fs.renameSync(extractedBinary, binaryPath);
254
+ const binaryName = NAME + (isWindows ? ".exe" : "");
255
+ const extractedBinary = path.join(tmpDir, `${NAME}-${platform}-${arch}`);
256
+ if (fs.existsSync(extractedBinary)) {
257
+ fs.copyFileSync(extractedBinary, dest);
258
+ } else {
259
+ // Fallback: try the plain name (older release format)
260
+ const plainBinary = path.join(tmpDir, binaryName);
261
+ if (fs.existsSync(plainBinary)) {
262
+ fs.copyFileSync(plainBinary, dest);
263
+ } else {
264
+ // Search in tmpDir
265
+ const files = fs.readdirSync(tmpDir).filter(f => f.includes(NAME) || f.includes(binaryName));
266
+ if (files.length > 0) {
267
+ fs.copyFileSync(path.join(tmpDir, files[0]), dest);
268
+ } else {
269
+ throw new Error(`Binary not found in extracted archive. Files: ${fs.readdirSync(tmpDir).join(", ")}`);
270
+ }
271
+ }
272
+ }
273
+ fs.chmodSync(dest, 0o755);
274
+ } finally {
275
+ fs.rmSync(tmpDir, { recursive: true, force: true });
338
276
  }
277
+ }
339
278
 
340
- if (!fs.existsSync(binaryPath)) {
341
- throw new Error(`Binary not found after extraction: ${binaryPath}`);
342
- }
279
+ // ── Entry ──────────────────────────────────────────────────────────────────
343
280
 
344
- if (platform !== 'windows') {
345
- fs.chmodSync(binaryPath, 0o755);
281
+ if (require.main === module) {
282
+ if (!platform || !arch) {
283
+ console.error(`Unsupported platform: ${process.platform}-${process.arch}`);
284
+ process.exit(1);
346
285
  }
347
286
 
348
- console.log(`[delta-cli] Installed at ${binaryPath}.`);
349
- }
287
+ // npx postinstall guard: skip binary download in ephemeral npx context
288
+ const isNpxPostinstall =
289
+ process.env.npm_command === "exec" && !process.env.DELTA_CLI_RUN;
350
290
 
351
- function handleError(err) {
352
- const fatal = process.env.DELTA_CLI_FATAL_ON_ERROR === '1';
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);
291
+ if (isNpxPostinstall) {
292
+ process.exit(0);
357
293
  }
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
294
 
364
- if (require.main === module) {
365
- // npx exec 上下文中跳过 postinstall 二进制下载
366
- // run.js 在需要时会设置 DELTA_CLI_RUN=true 再次调用
367
- if (process.env.npm_command === 'exec' && !process.env.DELTA_CLI_RUN) {
368
- console.log('[delta-cli] postinstall skipped (npx exec context).');
369
- process.exit(0);
295
+ try {
296
+ install();
297
+ console.log(`${NAME} v${VERSION} installed successfully`);
298
+ } catch (err) {
299
+ console.error(`Failed to install ${NAME}:`, err.message);
300
+ console.error(
301
+ `\nIf you are behind a firewall or in a restricted network, try one of:\n` +
302
+ ` # 1. Use a proxy:\n` +
303
+ ` export https_proxy=http://your-proxy:port\n` +
304
+ ` npm install -g @delta-infra/cli\n\n` +
305
+ ` # 2. Point to a corporate npm mirror:\n` +
306
+ ` npm install -g @delta-infra/cli --registry=https://your-corp-mirror/\n` +
307
+ `\nOr manually download from:\n` +
308
+ ` https://github.com/${REPO}/releases/tag/v${VERSION}`
309
+ );
310
+ process.exit(1);
370
311
  }
371
- install({ allowSkip: true }).catch(handleError);
372
312
  }
373
313
 
374
- module.exports = { install };
314
+ module.exports = {
315
+ getExpectedChecksum, verifyChecksum, assertAllowedHost,
316
+ isCurlVersionSupported, curlSupportsSslRevokeBestEffort,
317
+ };
package/scripts/run.js CHANGED
@@ -1,39 +1,65 @@
1
1
  #!/usr/bin/env node
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { execFileSync } = require('child_process');
2
+ // Copyright (c) 2026 Delta Infra Authors
3
+ // SPDX-License-Identifier: MIT
5
4
 
6
- const binary = path.join(
7
- __dirname, '..', 'bin',
8
- process.platform === 'win32' ? 'delta-cli.exe' : 'delta-cli'
9
- );
5
+ const { execFileSync } = require("child_process");
6
+ const fs = require("fs");
7
+ const path = require("path");
10
8
 
11
- const args = process.argv.slice(2);
9
+ const ext = process.platform === "win32" ? ".exe" : "";
10
+ const bin = path.join(__dirname, "..", "bin", "delta-cli" + ext);
12
11
 
13
- if (args[0] === 'install' || args[0] === 'uninstall') {
14
- require('./install-wizard');
15
- process.exit(0);
12
+ // On Windows, a crashed self-update may have left the binary renamed to .old.
13
+ const oldBin = bin + ".old";
14
+ function restoreOldBinary() {
15
+ try {
16
+ if (fs.existsSync(bin)) {
17
+ fs.rmSync(bin, { force: true });
18
+ }
19
+ fs.renameSync(oldBin, bin);
20
+ return true;
21
+ } catch { return false; }
16
22
  }
17
23
 
18
- if (!fs.existsSync(binary)) {
19
- console.error('[delta-cli] Binary not found, installing...');
20
- try {
21
- execFileSync(process.execPath, [path.join(__dirname, 'install.js')], {
22
- env: { ...process.env, DELTA_CLI_RUN: 'true' },
23
- stdio: 'inherit',
24
- });
25
- } catch (e) {
26
- console.error('[delta-cli] Could not install binary:', e.message);
27
- process.exit(1);
28
- }
29
- if (!fs.existsSync(binary)) {
30
- console.error('[delta-cli] Binary still missing after install attempt.');
31
- process.exit(1);
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
+ }
32
34
  }
33
35
  }
34
36
 
35
- try {
36
- execFileSync(binary, args, { stdio: 'inherit' });
37
- } catch (e) {
38
- process.exit(e.status || 1);
37
+ const args = process.argv.slice(2);
38
+
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", DELTA_CLI_VERSION: process.env.DELTA_CLI_VERSION || "" },
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
+ );
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ try {
61
+ execFileSync(bin, args, { stdio: "inherit" });
62
+ } catch (e) {
63
+ process.exit(e.status || 1);
64
+ }
39
65
  }