@delta-infra/cli 1.0.6 → 1.0.8

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` 会按以下顺序尝试下载当前平台的二进制:
52
+
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)。 |
33
76
 
34
- ### 方式三:Go Install
77
+ ### 方式四:Go Install
35
78
 
36
79
  ```bash
37
80
  go install github.com/delta-infra/delta-infra-cli/cmd/delta-cli@latest
@@ -45,21 +88,30 @@ 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`)以及 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
+ --token your-token
100
+
101
+ # 通过环境变量
102
+ export DELTA_INFRA_BASE_URL=http://your-server/api/v1
103
+ export DELTA_SANDBOX_TOKEN=your-token
104
+ delta-cli config init
52
105
  ```
53
106
 
107
+ 配置文件保存在 `~/.delta-infra/config.json`(权限 0600)。
108
+
54
109
  ### 2. 认证
55
110
 
56
- 支持 API Key 或 Bearer Token:
111
+ 使用 Bearer Token:
57
112
 
58
113
  ```bash
59
- # API Key
60
- delta-cli auth login --api-key <your-api-key>
61
-
62
- # Bearer Token
114
+ # Token
63
115
  delta-cli auth login --token <your-token>
64
116
 
65
117
  # 查看认证状态
@@ -96,7 +148,6 @@ delta-cli sandbox kill <sandbox_id>
96
148
  | `delta-cli config show` | 查看当前配置(敏感字段已脱敏) |
97
149
  | `delta-cli config set <key> <value>` | 修改配置项 |
98
150
  | `delta-cli auth login` | 交互式登录 |
99
- | `delta-cli auth login --api-key <key>` | API Key 认证 |
100
151
  | `delta-cli auth login --token <token>` | Token 认证 |
101
152
  | `delta-cli auth status` | 查看认证状态 |
102
153
  | `delta-cli update check` | 检查更新 |
@@ -107,6 +158,7 @@ delta-cli sandbox kill <sandbox_id>
107
158
 
108
159
  | 命令 | 说明 |
109
160
  |------|------|
161
+ | `delta-cli sandbox list` | 列出当前用户的 sandbox |
110
162
  | `delta-cli sandbox create --image <img>` | 创建 sandbox 容器 |
111
163
  | `delta-cli sandbox connect <id>` | 连接 sandbox |
112
164
  | `delta-cli sandbox status <id>` | 查看 sandbox 状态 |
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.8",
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`
@@ -1,125 +1,84 @@
1
1
  ---
2
2
  name: delta-shared
3
- version: 1.0.0
4
- description: "delta-cli 全局共享规则。使用于首次配置、认证登录、权限不足恢复、配置查看、遇到 JSON 输出中的 _notice 或通用错误处理时。"
3
+ version: 1.0.8
4
+ description: "delta-cli 全局共享规则:配置/认证检查、安全红线、错误处理、sandbox 销毁。"
5
5
  ---
6
6
 
7
7
  # delta-cli 共享规则
8
8
 
9
- 本技能指导你如何通过 delta-cli 操作 Delta Sandbox 及相关资源,以及所有命令域通用的注意事项。
9
+ 指导 AI 使用 `delta-cli` 的通用规则。核心原则:**只检查、不猜测、不泄露密钥**。
10
10
 
11
- ## 获取服务端地址(必须优先执行)
11
+ ## 执行任何操作之前
12
12
 
13
- 所有操作的前提是知道 delta-sandbox-server 的地址。按以下步骤获取:
13
+ 1. 确认服务端地址:
14
+ ```bash
15
+ delta-cli config show
16
+ ```
17
+ 2. 确认认证状态:
18
+ ```bash
19
+ delta-cli auth status
20
+ ```
14
21
 
15
- 1. 执行 `echo $DELTA_INFRA_BASE_URL` 检查环境变量是否已设置
16
- 2. 如果输出非空,说明已配置,直接使用,无需额外操作
17
- 3. 如果为空,**向用户询问真实地址**,然后运行 `delta-cli config set base_url <url>` 保存
22
+ - 若 `auth status` 显示 `method: token`,**直接执行目标命令**,不要重复登录或初始化配置。
23
+ - 若显示 `method: none`,或操作返回 `error.type: auth` / `permission`,才进入下面的“补全配置”流程。
18
24
 
19
- **不要猜测或虚构地址。不确认地址就执行任何操作会导致 network 错误。**
25
+ ## 补全配置
20
26
 
21
- ## 配置初始化
22
-
23
- 首次使用需运行 `delta-cli config init` 完成配置:
24
-
25
- ```bash
26
- delta-cli config init
27
- ```
28
-
29
- 这会创建 `~/.delta-infra/config.json`,需按实际部署设置 base_url(`delta-cli config set base_url <url>`),也可通过环境变量 `DELTA_INFRA_BASE_URL` 覆盖。
30
-
31
- ## 配置管理
27
+ 缺少服务端地址或凭证时,**向用户询问一次**,然后用 flag 初始化:
32
28
 
33
29
  ```bash
34
- delta-cli config show
35
- delta-cli config set base_url <url>
36
- delta-cli config set api_key <key>
37
- delta-cli config set token <token>
38
- delta-cli config remove api_key
39
- delta-cli config remove token
30
+ delta-cli config init --base-url <server-url> --token <token>
40
31
  ```
41
32
 
42
- ## 环境变量覆盖
43
-
44
- 以下环境变量优先级高于配置文件:
45
-
46
- | 变量 | 说明 |
47
- |------|------|
48
- | `DELTA_INFRA_BASE_URL` | 覆盖 base_url |
49
- | `DELTA_SANDBOX_API_KEY` | 覆盖 api_key |
50
- | `DELTA_SANDBOX_TOKEN` | 覆盖 token |
51
-
52
- ## 认证
53
-
54
- delta-cli **不再内嵌默认凭证**,使用前必须配置认证。
33
+ 不要用交互式 `config init`(无 flag),避免在 stdin 不可用时卡住。
55
34
 
56
- 支持两种认证方式,优先级从高到低:
35
+ ### 信息来源优先级
57
36
 
58
- | 方式 | 命令 / 来源 | 适用场景 |
59
- |------|------------|----------|
60
- | API Key | `delta-cli auth login --api-key <key>` 或环境变量 `DELTA_SANDBOX_API_KEY` | 推荐 |
61
- | Bearer Token | `delta-cli auth login --token <token>` 或环境变量 `DELTA_SANDBOX_TOKEN` | 使用已有 token |
37
+ 1. 环境变量:`DELTA_INFRA_BASE_URL`、`DELTA_SANDBOX_TOKEN`
38
+ 2. `~/.delta-infra/config.json` 中的 `base_url` / `token`
39
+ 3. 默认值:仅 `base_url` 有内部默认值
40
+ 4. 以上都缺失时,**必须询问用户**,禁止虚构
62
41
 
63
- ### 默认认证说明
42
+ ### 运行时凭证
64
43
 
65
- - binary 中没有默认 token,第一次使用**必须**执行 `delta-cli auth login` 或设置环境变量。
66
- - 如需更换账号,重新执行 `delta-cli auth login --token <token>` 或 `delta-cli auth login --api-key <key>`。
67
- - 未配置时 `auth status` 会显示 `method: none`。
44
+ CLI 使用 `token` 发送请求头 `Authorization: Bearer <token>`,并自动从 JWT 中提取 `uid` 作为 `X-User-Uid`。
68
45
 
69
- ### 查看认证状态
46
+ ## 安全红线
70
47
 
71
- ```bash
72
- delta-cli auth status
73
- ```
74
-
75
- ### 权限不足处理
76
-
77
- 遇到认证/权限错误时(`error.type: auth` 或 `permission`):
78
-
79
- 1. 先执行 `delta-cli auth status` 确认当前认证状态
80
- 2. 如需更换认证,执行 `delta-cli auth login --token <token>` 或 `delta-cli auth login --api-key <key>`
81
- 3. 如已认证但仍报错,检查 base_url 是否正确
82
-
83
- **禁止**:将 API Key 或 Token 输出到终端明文展示给用户。
84
-
85
- ## 安全规则
86
-
87
- - **禁止输出密钥**:`api_key`、`token` 不得在日志、消息或终端中明文展示。
88
- - **写入/删除操作前必须确认用户意图**:`sandbox kill`、`sandbox finish`、`sandbox write` 等操作前需明确告知用户影响。
89
- - **配置文件权限**:`~/.delta-infra/config.json` 权限为 `0600`,严禁手动放宽。
90
- - **sandbox 必须销毁**:调用 `sandbox create` 后,当前任务结束前必须调用 `sandbox kill`。server 端无自动清理,不销毁 = 永久占用 GPU。即使遇到错误也不得跳过销毁。
48
+ - **禁止输出密钥**:`token` 不得出现在日志、消息、终端。
49
+ - **禁止自己拼 URL / 构造 HTTP 请求**:所有请求都走 `delta-cli`。
50
+ - **不要主动安装 CLI**:如果用户说没装,建议他去 README 或 install.sh,不要自己选安装方式。
51
+ - **写入/删除操作前确认意图**:`sandbox kill`、`sandbox finish`、`sandbox write` 等必须告诉用户影响。
52
+ - `~/.delta-infra/config.json` 权限为 `0600`,不要手动放宽。
53
+ - **sandbox 必须销毁**:调用 `sandbox create` 后,当前任务结束前必须调用 `sandbox kill`。server 端无自动清理,不销毁 = 永久占用 GPU;即使出错也不得跳过。
91
54
 
92
55
  ## JSON 输出约定
93
56
 
94
- 所有命令输出遵循统一 JSON envelope:
95
-
96
57
  ```json
97
58
  {"ok":true,"data":{...}}
98
59
  {"ok":false,"error":{"type":"auth","message":"...","hint":"..."}}
99
60
  ```
100
61
 
101
62
  AI Agent 解析规则:
102
- - 始终检查 `ok` 字段,不要仅依赖 exit code
103
- - `error.type` 是稳定分支字段:`auth`, `permission`, `not_found`, `validation`, `network`, `api`, `internal`
104
- - `error.hint` 是可读修复建议,可展示给用户
63
+ - 始终检查 `ok`,不要只看 exit code。
64
+ - `error.type` 是稳定分支字段:`auth`, `permission`, `not_found`, `validation`, `network`, `api`, `internal`。
65
+ - `error.hint` 可展示给用户。
105
66
 
106
67
  ## 退出码约定
107
68
 
108
69
  | Type | Exit Code | 场景 |
109
70
  |------|-----------|------|
110
- | validation | 2 | flag 错误、参数缺失 |
71
+ | validation | 2 | flag / 参数错误 |
111
72
  | auth | 3 | 未认证或认证失败 |
112
73
  | permission | 4 | 权限不足 |
113
74
  | not_found | 5 | sandbox/file 不存在 |
114
75
  | network | 6 | DNS、超时、连接失败 |
115
- | api | 7 | 服务端非 2xx 错误 |
76
+ | api | 7 | 服务端非 2xx |
116
77
  | internal | 10 | 客户端内部错误 |
117
78
 
118
79
  ## 更新检查
119
80
 
120
- `delta-cli update check` 检查最新版本。
121
-
122
- **当你在 JSON 中看到 `_notice.update` 时**,完成用户当前请求后,主动提议更新:
81
+ 完成用户请求后,如果 JSON 中有 `_notice.update`,可建议:
123
82
 
124
83
  ```bash
125
84
  delta-cli update
@@ -129,8 +88,9 @@ delta-cli update
129
88
 
130
89
  | 错误 / 现象 | 恢复动作 |
131
90
  |-------------|----------|
132
- | `error.type: auth` | 先执行 `auth status` 确认;如无认证或需更换,执行 `auth login --token <token>` |
133
- | `error.type: not_found` | 检查 sandbox_id 或 path 是否正确 |
134
- | `error.type: network` | 检查 base_url 和网络连通性 |
91
+ | `error.type: auth` / `method: none` | 询问用户后执行 `config init --base-url <url> --token <token>` |
92
+ | `error.type: permission` | 确认当前认证是否有权限;必要时换 token |
93
+ | `error.type: not_found` | 检查 `sandbox_id` / path |
94
+ | `error.type: network` | 检查 `base_url` 和网络连通性 |
135
95
  | `error.type: validation` | 检查必填 flag 和参数格式 |
136
96
  | 配置文件解析失败 | 检查 `~/.delta-infra/config.json` 是否为有效 JSON |