@delta-infra/cli 1.0.6 → 1.0.7

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 CHANGED
@@ -10,28 +10,71 @@ Delta Infra 的命令行工具。
10
10
 
11
11
  ```bash
12
12
  # macOS (Apple Silicon)
13
- curl -L https://github.com/yzailab/delta-infra-cli/releases/latest/download/delta-cli-darwin-arm64.tar.gz | tar -xz
13
+ curl -L -o delta-cli.tar.gz https://github.com/yzailab/delta-infra-cli/releases/latest/download/delta-cli-darwin-arm64.tar.gz
14
+ tar -xzf delta-cli.tar.gz
14
15
  sudo mv delta-cli-darwin-arm64 /usr/local/bin/delta-cli
16
+ rm delta-cli.tar.gz
15
17
 
16
18
  # Linux (AMD64)
17
- curl -L https://github.com/yzailab/delta-infra-cli/releases/latest/download/delta-cli-linux-amd64.tar.gz | tar -xz
19
+ curl -L -o delta-cli.tar.gz https://github.com/yzailab/delta-infra-cli/releases/latest/download/delta-cli-linux-amd64.tar.gz
20
+ tar -xzf delta-cli.tar.gz
18
21
  chmod +x delta-cli-linux-amd64
19
22
  sudo mv delta-cli-linux-amd64 /usr/local/bin/delta-cli
23
+ rm delta-cli.tar.gz
24
+
25
+ # Windows (AMD64,PowerShell)
26
+ Invoke-WebRequest -Uri https://github.com/yzailab/delta-infra-cli/releases/latest/download/delta-cli-windows-amd64.zip -OutFile delta-cli.zip
27
+ Expand-Archive -Path delta-cli.zip -DestinationPath . -Force
28
+ Move-Item -Path delta-cli-windows-amd64.exe -Destination delta-cli.exe
29
+ Remove-Item -Path delta-cli.zip
30
+ # 将 delta-cli.exe 放到 PATH 中的目录
31
+ ```
32
+
33
+ ### 方式二:一键安装脚本(国内用户推荐)
34
+
35
+ 安装脚本会自动尝试国内 npm 镜像(npmmirror),失败后再回退到 npm 官方源:
36
+
37
+ ```bash
38
+ # Linux / macOS
39
+ curl -L https://raw.githubusercontent.com/yzailab/delta-infra-cli/main/install.sh | bash
20
40
 
21
- # Windows (AMD64)
22
- # 下载 https://github.com/yzailab/delta-infra-cli/releases/latest/download/delta-cli-windows-amd64.zip
23
- # 解压后将 delta-cli-windows-amd64.exe 重命名为 delta-cli.exe,并放到 PATH 目录下
41
+ # Windows (PowerShell)
42
+ Invoke-RestMethod -Uri https://raw.githubusercontent.com/yzailab/delta-infra-cli/main/install.ps1 | Invoke-Expression
24
43
  ```
25
44
 
26
- ### 方式二:npm
45
+ ### 方式三:npm
27
46
 
28
47
  ```bash
29
48
  npm install -g @delta-infra/cli
30
49
  ```
31
50
 
32
- 安装时会自动从 GitHub Release 下载当前平台的二进制文件并解压到本地。
51
+ `postinstall` 会按以下顺序尝试下载当前平台的二进制:
33
52
 
34
- ### 方式三:Go Install
53
+ 1. `DELTA_CLI_MIRROR` 环境变量(如果设置)
54
+ 2. 国内加速镜像 `https://gh-proxy.com/https://github.com/...`
55
+ 3. GitHub 源站 `https://github.com/yzailab/delta-infra-cli/releases/download/...`
56
+
57
+ 如果安装时下载失败,可以:
58
+
59
+ - 直接运行一次 `delta-cli`,它会自动重新尝试下载。
60
+ - 或者在安装前设置本地压缩包路径:
61
+
62
+ ```bash
63
+ DELTA_CLI_ARCHIVE=/path/to/delta-cli-linux-amd64.tar.gz npm install -g @delta-infra/cli
64
+ ```
65
+
66
+ 常用环境变量:
67
+
68
+ | 变量 | 说明 |
69
+ |------|------|
70
+ | `DELTA_CLI_MIRROR` | 下载镜像,例如 `https://gh-proxy.com/https://github.com`。只接受 HTTPS 且需在白名单内。 |
71
+ | `DELTA_CLI_MIRROR_ALLOWLIST` | 额外允许的主机列表,逗号分隔。 |
72
+ | `DELTA_CLI_ARCHIVE` | 指向预先下载好的 `.tar.gz` / `.zip` 本地路径,跳过网络下载。 |
73
+ | `DELTA_CLI_SKIP_POSTINSTALL` | 设置为 `1` 跳过安装时的二进制下载,之后由 `delta-cli` 运行时兜底。 |
74
+ | `DELTA_CLI_FATAL_ON_ERROR` | 安装失败时退出码非 0(默认警告,用于调试)。 |
75
+ | `DELTA_CLI_DOWNLOAD_TIMEOUT` | 单个下载源最大等待时间(毫秒,默认 120000)。 |
76
+
77
+ ### 方式四:Go Install
35
78
 
36
79
  ```bash
37
80
  go install github.com/delta-infra/delta-infra-cli/cmd/delta-cli@latest
@@ -45,12 +88,25 @@ go install github.com/delta-infra/delta-infra-cli/cmd/delta-cli@latest
45
88
  delta-cli config init
46
89
  ```
47
90
 
48
- 配置文件保存在 `~/.delta-infra/config.json`(权限 0600),通过 `delta-cli config set base_url <url>` 配置服务端地址。也可通过环境变量覆盖:
91
+ 执行时会交互式提示输入服务端地址(默认:`http://172.17.152.6:8000/api/v1`)以及 API Key / Bearer Token(可选,也可后续用 `auth login` 配置)。
92
+
93
+ 非交互式环境(如 CI)会直接使用默认值,也可以通过 flag 或环境变量一次性指定:
49
94
 
50
95
  ```bash
51
- export DELTA_INFRA_BASE_URL=<url>
96
+ # 通过 flag
97
+ delta-cli config init \
98
+ --base-url http://your-server/api/v1 \
99
+ --api-key your-api-key
100
+
101
+ # 通过环境变量
102
+ export DELTA_INFRA_BASE_URL=http://your-server/api/v1
103
+ export DELTA_SANDBOX_API_KEY=your-api-key
104
+ export DELTA_SANDBOX_TOKEN=your-token
105
+ delta-cli config init
52
106
  ```
53
107
 
108
+ 配置文件保存在 `~/.delta-infra/config.json`(权限 0600)。
109
+
54
110
  ### 2. 认证
55
111
 
56
112
  支持 API Key 或 Bearer Token:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delta-infra/cli",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Delta Sandbox CLI",
5
5
  "bin": {
6
6
  "delta-cli": "scripts/run.js"
@@ -9,6 +9,7 @@
9
9
  "postinstall": "node scripts/install.js"
10
10
  },
11
11
  "files": [
12
+ "checksums.txt",
12
13
  "scripts/install.js",
13
14
  "scripts/run.js",
14
15
  "skills/delta-sandbox/SKILL.md",
@@ -16,6 +17,15 @@
16
17
  "skills/delta-shared/SKILL.md",
17
18
  "skills/delta-shared/references/"
18
19
  ],
20
+ "os": [
21
+ "darwin",
22
+ "linux",
23
+ "win32"
24
+ ],
25
+ "cpu": [
26
+ "x64",
27
+ "arm64"
28
+ ],
19
29
  "keywords": [
20
30
  "delta",
21
31
  "sandbox",
@@ -3,19 +3,23 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const https = require('https');
5
5
  const crypto = require('crypto');
6
- const { execSync } = require('child_process');
6
+ const { execSync, spawn } = require('child_process');
7
7
 
8
8
  const BINARY_NAME = 'delta-cli';
9
9
  const VERSION = require('../package.json').version;
10
- const RELEASE_BASE_URL = 'https://github.com/yzailab/delta-infra-cli/releases/download';
11
10
 
12
- function releaseAssetUrl(version, assetName) {
13
- const mirror = process.env.DELTA_CLI_MIRROR || '';
14
- if (mirror) {
15
- return `${mirror.replace(/\/$/, '')}/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`;
16
- }
17
- return `${RELEASE_BASE_URL}/v${version}/${assetName}`;
18
- }
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
+ ];
19
+ const DEFAULT_ALLOWED_HOST_PREFIXES = [
20
+ 'github-production-release-asset-',
21
+ ];
22
+ const MAX_REDIRECTS = 5;
19
23
 
20
24
  const platformMap = {
21
25
  darwin: 'darwin',
@@ -28,6 +32,20 @@ const archMap = {
28
32
  arm64: 'arm64',
29
33
  };
30
34
 
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';
37
+
38
+ function releaseAssetUrls(version, assetName) {
39
+ const urls = [];
40
+ const mirror = process.env.DELTA_CLI_MIRROR || '';
41
+ if (mirror) {
42
+ urls.push(`${mirror.replace(/\/$/, '')}/yzailab/delta-infra-cli/releases/download/v${version}/${assetName}`);
43
+ }
44
+ urls.push(`${RELEASE_DOMESTIC_BASE}/v${version}/${assetName}`);
45
+ urls.push(`${RELEASE_SOURCE_BASE}/v${version}/${assetName}`);
46
+ return urls;
47
+ }
48
+
31
49
  function resolveUrl(base, location) {
32
50
  if (!location) return base;
33
51
  if (location.startsWith('http://') || location.startsWith('https://')) {
@@ -37,55 +55,239 @@ function resolveUrl(base, location) {
37
55
  return new URL(location, baseUrl).href;
38
56
  }
39
57
 
40
- function download(url, dest) {
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) {
41
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();
105
+ const allowlist = new Set(DEFAULT_ALLOWED_HOSTS);
106
+ (process.env.DELTA_CLI_MIRROR_ALLOWLIST || '')
107
+ .split(',')
108
+ .map((h) => h.trim().toLowerCase())
109
+ .filter(Boolean)
110
+ .forEach((h) => allowlist.add(h));
111
+ for (const allowed of allowlist) {
112
+ if (host === allowed || host.endsWith('.' + allowed)) {
113
+ return true;
114
+ }
115
+ }
116
+ for (const prefix of DEFAULT_ALLOWED_HOST_PREFIXES) {
117
+ if (host.startsWith(prefix)) {
118
+ return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
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
+ }
133
+
134
+ const DOWNLOAD_TIMEOUT_MS = parseInt(process.env.DELTA_CLI_DOWNLOAD_TIMEOUT, 10) || 120000;
135
+
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
+ }
146
+
42
147
  const file = fs.createWriteStream(dest);
43
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);
44
153
 
45
- const cleanup = () => {
46
- try { fs.unlinkSync(dest); } catch {}
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);
47
163
  };
48
164
 
49
- https.get(url, { timeout: 60000 }, (response) => {
50
- if (response.statusCode >= 301 && response.statusCode <= 308 && response.headers.location) {
51
- file.destroy();
52
- cleanup();
53
- download(resolveUrl(url, response.headers.location), dest).then(resolve).catch(reject);
54
- return;
55
- }
56
- if (response.statusCode !== 200) {
57
- file.destroy();
58
- cleanup();
59
- reject(new Error(`Download failed: ${response.statusCode}`));
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
+ });
205
+ }
206
+
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})`));
60
222
  return;
61
223
  }
62
- response.pipe(file);
63
- response.on('data', (chunk) => hash.update(chunk));
64
- file.on('finish', () => {
65
- file.close(() => resolve(hash.digest('hex')));
66
- });
67
- }).on('error', (err) => {
68
- file.destroy();
69
- cleanup();
70
- reject(err);
224
+ fileSha256(dest).then(resolve).catch(reject);
71
225
  });
72
226
  });
73
227
  }
74
228
 
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');
233
+
234
+ if (preferCurl) {
235
+ try {
236
+ return await curlDownload(url, dest, proxy);
237
+ } catch (err) {
238
+ if (process.env.DELTA_CLI_USE_CURL === '1') {
239
+ throw err;
240
+ }
241
+ console.warn(`[delta-cli] curl download failed (${err.message}), falling back to Node https.`);
242
+ }
243
+ }
244
+
245
+ return nodeDownload(url, dest);
246
+ }
247
+
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`);
256
+ }
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}`);
260
+ }
261
+ console.log(`[delta-cli] Checksum OK for ${assetName}.`);
262
+ }
263
+
75
264
  function extract(archivePath, destDir, platform) {
265
+ fs.mkdirSync(destDir, { recursive: true });
76
266
  if (platform === 'windows') {
77
- execSync(`unzip -o "${archivePath}" -d "${destDir}"`, { stdio: 'inherit' });
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' }
272
+ );
78
273
  } else {
79
274
  execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' });
80
275
  }
81
276
  }
82
277
 
83
- async function main() {
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
+ }
286
+
84
287
  const platform = platformMap[process.platform];
85
288
  const arch = archMap[process.arch];
86
289
  if (!platform || !arch) {
87
- console.error(`Unsupported platform: ${process.platform} ${process.arch}`);
88
- process.exit(1);
290
+ throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
89
291
  }
90
292
 
91
293
  const isWin = platform === 'windows';
@@ -98,25 +300,69 @@ async function main() {
98
300
 
99
301
  fs.mkdirSync(binDir, { recursive: true });
100
302
 
101
- const url = releaseAssetUrl(VERSION, assetName);
102
- console.log(`Downloading ${assetName} from ${process.env.DELTA_CLI_MIRROR ? 'mirror' : 'GitHub'}...`);
103
-
104
- try {
105
- const checksum = await download(url, archivePath);
106
- console.log(`Downloaded archive, SHA256: ${checksum}`);
107
- console.log(`Extracting to ${binDir}...`);
108
- extract(archivePath, binDir, platform);
109
- const extractedBinary = path.join(binDir, `${BINARY_NAME}-${platform}-${arch}${binaryExt}`);
110
- if (fs.existsSync(extractedBinary) && extractedBinary !== binaryPath) {
111
- fs.renameSync(extractedBinary, binaryPath);
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);
311
+ let lastErr;
312
+ let checksum;
313
+ for (const url of urls) {
314
+ console.log(`[delta-cli] Downloading ${assetName} from ${new URL(url).hostname}...`);
315
+ try {
316
+ checksum = await download(url, archivePath);
317
+ break;
318
+ } catch (err) {
319
+ console.warn(`[delta-cli] Download failed from ${url}: ${err.message}`);
320
+ lastErr = err;
321
+ }
112
322
  }
323
+ if (checksum === undefined) {
324
+ throw lastErr || new Error('All download sources failed');
325
+ }
326
+ }
327
+
328
+ const checksums = loadChecksums();
329
+ verifyAsset(assetName, archivePath, checksums);
330
+
331
+ console.log(`[delta-cli] Extracting to ${binDir}...`);
332
+ extract(archivePath, binDir, platform);
333
+ fs.unlinkSync(archivePath);
334
+
335
+ const extractedBinary = path.join(binDir, `${BINARY_NAME}-${platform}-${arch}${binaryExt}`);
336
+ if (fs.existsSync(extractedBinary) && extractedBinary !== binaryPath) {
337
+ fs.renameSync(extractedBinary, binaryPath);
338
+ }
339
+
340
+ if (!fs.existsSync(binaryPath)) {
341
+ throw new Error(`Binary not found after extraction: ${binaryPath}`);
342
+ }
343
+
344
+ if (platform !== 'windows') {
113
345
  fs.chmodSync(binaryPath, 0o755);
114
- fs.unlinkSync(archivePath);
115
- console.log(`Installed to ${binaryPath}`);
116
- } catch (err) {
117
- console.error('Install failed:', err.message);
346
+ }
347
+
348
+ console.log(`[delta-cli] Installed at ${binaryPath}.`);
349
+ }
350
+
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);
118
356
  process.exit(1);
119
357
  }
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
+
364
+ if (require.main === module) {
365
+ install({ allowSkip: true }).catch(handleError);
120
366
  }
121
367
 
122
- main();
368
+ module.exports = { install };
package/scripts/run.js CHANGED
@@ -1,21 +1,60 @@
1
1
  #!/usr/bin/env node
2
+ const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { spawn } = require('child_process');
5
+ const { install } = require('./install');
4
6
 
5
- const platform = process.platform === 'win32' ? 'windows' : process.platform;
6
- const ext = platform === 'windows' ? '.exe' : '';
7
- const binary = path.join(__dirname, '..', 'bin', `delta-cli${ext}`);
7
+ const binary = path.join(
8
+ __dirname,
9
+ '..',
10
+ 'bin',
11
+ process.platform === 'win32' ? 'delta-cli.exe' : 'delta-cli'
12
+ );
8
13
 
9
- const child = spawn(binary, process.argv.slice(2), {
10
- stdio: 'inherit',
11
- windowsHide: true,
12
- });
14
+ async function ensureBinary() {
15
+ if (fs.existsSync(binary)) {
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
+ }
13
24
 
14
- child.on('error', (err) => {
15
- console.error('Failed to start delta-cli:', err.message);
16
- process.exit(1);
17
- });
25
+ async function main() {
26
+ try {
27
+ await ensureBinary();
28
+ } catch (err) {
29
+ console.error('[delta-cli] Could not install binary:', err.message);
30
+ console.error('Set DELTA_CLI_ARCHIVE to a local archive path, or reinstall the package.');
31
+ process.exit(1);
32
+ }
18
33
 
19
- child.on('exit', (code) => {
20
- process.exit(code ?? 0);
21
- });
34
+ const child = spawn(binary, process.argv.slice(2), {
35
+ stdio: 'inherit',
36
+ windowsHide: true,
37
+ });
38
+
39
+ child.on('error', (err) => {
40
+ console.error('[delta-cli] Failed to start:', err.message);
41
+ process.exit(1);
42
+ });
43
+
44
+ child.on('close', (code, signal) => {
45
+ if (code !== null && code !== undefined) {
46
+ process.exit(code);
47
+ }
48
+ if (signal) {
49
+ try {
50
+ process.kill(process.pid, signal);
51
+ } catch {
52
+ process.exit(1);
53
+ }
54
+ } else {
55
+ process.exit(1);
56
+ }
57
+ });
58
+ }
59
+
60
+ main();
@@ -54,6 +54,7 @@ metadata:
54
54
 
55
55
  1. **查看可用镜像**:`delta-cli sandbox images` — 查询服务端支持的镜像列表(含标签、最低资源要求、支持 provider),根据用户需求匹配镜像
56
56
  2. **创建**:`delta-cli sandbox create --image <img> --cpu 4 --memory 16Gi --gpu 1 --gpu-mem 8000`(创建后 sandbox 立即可用,无需额外连接)。**同一次任务若已有 `sandbox_id`,禁止再次 create,必须优先复用。**
57
+ - 这是 `sandbox create` 支持的完整资源参数集合,不存在其它“更正确”的资源 flag,不要 invented 不存在的参数。
57
58
  3. **写入代码**:`delta-cli sandbox write <id> --path /workspace/train.py --data "..."`
58
59
  4. **运行**:`delta-cli sandbox run <id> --command "python /workspace/train.py" --timeout 3600`
59
60
  5. **读取结果**:`delta-cli sandbox read <id> --path /workspace/result.json`
@@ -6,7 +6,7 @@
6
6
 
7
7
  1. **查看可用镜像** `delta-cli sandbox images` — 查询服务端支持的镜像列表,根据用户的 GPU/CPU 需求和标签匹配镜像
8
8
  2. **列出现有 sandbox(可选)** `delta-cli sandbox list` — 查看当前用户已创建的活跃 sandbox,避免重复创建
9
- 3. **创建** `delta-cli sandbox create --image <image> --cpu 4 --memory 16Gi --gpu 1 --gpu-mem 8000`(创建后立即可用,无需连接)
9
+ 3. **创建** `delta-cli sandbox create --image <image> --cpu 4 --memory 16Gi --gpu 1 --gpu-mem 8000`(创建后立即可用,无需连接;这些是 `create` 全部资源参数,不要 invented 其它 flag)
10
10
  4. **写入代码/数据** `delta-cli sandbox write <id> --path /workspace/train.py --data "..."`
11
11
  5. **运行命令** `delta-cli sandbox run <id> --command "python /workspace/train.py"`
12
12
  6. **读取结果** `delta-cli sandbox read <id> --path /workspace/result.json`
@@ -8,25 +8,39 @@ description: "delta-cli 全局共享规则。使用于首次配置、认证登
8
8
 
9
9
  本技能指导你如何通过 delta-cli 操作 Delta Sandbox 及相关资源,以及所有命令域通用的注意事项。
10
10
 
11
- ## 获取服务端地址(必须优先执行)
11
+ ## 获取服务端地址
12
12
 
13
- 所有操作的前提是知道 delta-sandbox-server 的地址。按以下步骤获取:
13
+ 所有操作的前提是知道 delta-sandbox-server 的地址。按以下优先级获取:
14
14
 
15
- 1. 执行 `echo $DELTA_INFRA_BASE_URL` 检查环境变量是否已设置
16
- 2. 如果输出非空,说明已配置,直接使用,无需额外操作
17
- 3. 如果为空,**向用户询问真实地址**,然后运行 `delta-cli config set base_url <url>` 保存
15
+ 1. 环境变量 `DELTA_INFRA_BASE_URL`(优先级最高)
16
+ 2. `~/.delta-infra/config.json` 中的 `base_url`
17
+ 3. 以上都为空时,**向用户询问真实地址**,再运行 `delta-cli config set base_url <url>` 保存
18
18
 
19
- **不要猜测或虚构地址。不确认地址就执行任何操作会导致 network 错误。**
19
+ **不要猜测或虚构地址。** 已有配置文件时直接使用,只有配置文件不存在或操作报 `network` 错误时才需要重新确认。
20
20
 
21
21
  ## 配置初始化
22
22
 
23
- 首次使用需运行 `delta-cli config init` 完成配置:
23
+ 如果 `~/.delta-infra/config.json` 已存在且包含有效的 `base_url` 与认证信息,则**无需执行** `config init`。
24
+
25
+ 仅在以下情况才运行初始化:
26
+
27
+ - 配置文件不存在
28
+ - 配置文件为空或解析失败
29
+ - 用户明确要求重新初始化
30
+
31
+ 首次使用或有上述情况时运行:
24
32
 
25
33
  ```bash
26
34
  delta-cli config init
27
35
  ```
28
36
 
29
- 这会创建 `~/.delta-infra/config.json`,需按实际部署设置 base_url(`delta-cli config set base_url <url>`),也可通过环境变量 `DELTA_INFRA_BASE_URL` 覆盖。
37
+ 在交互式终端上,它会提示输入服务端地址(直接回车使用默认值)以及可选的 API Key / Bearer Token;AI 工具或非交互场景应使用 flag/环境变量,避免在 stdin 不可用时卡住:
38
+
39
+ ```bash
40
+ delta-cli config init --base-url <url> --api-key <key> --token <token>
41
+ ```
42
+
43
+ 配置还可以通过环境变量 `DELTA_INFRA_BASE_URL`、`DELTA_SANDBOX_API_KEY`、`DELTA_SANDBOX_TOKEN` 提供。
30
44
 
31
45
  ## 配置管理
32
46
 
@@ -51,7 +65,24 @@ delta-cli config remove token
51
65
 
52
66
  ## 认证
53
67
 
54
- delta-cli **不再内嵌默认凭证**,使用前必须配置认证。
68
+ delta-cli **不再内嵌默认凭证**,使用前必须存在有效认证。
69
+
70
+ ### 优先检查,避免重复登录
71
+
72
+ 执行任何需要认证的操作前,先查状态:
73
+
74
+ ```bash
75
+ # 1. 确认服务端地址
76
+ delta-cli config show
77
+
78
+ # 2. 确认认证是否已生效
79
+ delta-cli auth status
80
+ ```
81
+
82
+ - 如果 `auth status` 显示 `method: token` 或 `method: api_key`,说明配置文件已存在有效凭证,**直接执行目标操作**,不要重复 `auth login`。
83
+ - 只有当 `auth status` 显示 `method: none`、token 已过期,或操作返回 `error.type: auth` / `permission` 时,才需要重新登录。
84
+
85
+ ### 认证方式
55
86
 
56
87
  支持两种认证方式,优先级从高到低:
57
88
 
@@ -62,21 +93,15 @@ delta-cli **不再内嵌默认凭证**,使用前必须配置认证。
62
93
 
63
94
  ### 默认认证说明
64
95
 
65
- - binary 中没有默认 token,第一次使用**必须**执行 `delta-cli auth login` 或设置环境变量。
96
+ - binary 中没有默认 token。
97
+ - 第一次使用或认证失效时,执行 `delta-cli auth login` 或设置对应环境变量。
66
98
  - 如需更换账号,重新执行 `delta-cli auth login --token <token>` 或 `delta-cli auth login --api-key <key>`。
67
- - 未配置时 `auth status` 会显示 `method: none`。
68
-
69
- ### 查看认证状态
70
-
71
- ```bash
72
- delta-cli auth status
73
- ```
74
99
 
75
100
  ### 权限不足处理
76
101
 
77
102
  遇到认证/权限错误时(`error.type: auth` 或 `permission`):
78
103
 
79
- 1. 先执行 `delta-cli auth status` 确认当前认证状态
104
+ 1. 执行 `delta-cli auth status` 确认当前认证状态
80
105
  2. 如需更换认证,执行 `delta-cli auth login --token <token>` 或 `delta-cli auth login --api-key <key>`
81
106
  3. 如已认证但仍报错,检查 base_url 是否正确
82
107