@dreamlogic-ai/cli 2.0.5 → 2.0.6

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.
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as clack from "@clack/prompts";
6
6
  import { join } from "path";
7
+ import { statfsSync } from "fs";
7
8
  import { ApiClient } from "../lib/api-client.js";
8
9
  import { getServer, getInstallDir, loadInstalled } from "../lib/config.js";
9
10
  import { installSkill } from "../lib/installer.js";
@@ -110,6 +111,25 @@ export async function installCommand(skillIds, opts) {
110
111
  return;
111
112
  }
112
113
  }
114
+ // R4-FIX #8: 安装前检查磁盘空间(可选警告)
115
+ try {
116
+ const totalSize = selectedIds.reduce((sum, id) => {
117
+ const s = skills.find((s) => s.id === id);
118
+ return sum + (s?.package_size || 0);
119
+ }, 0);
120
+ if (totalSize > 0) {
121
+ const installPath = getInstallDir();
122
+ const { mkdirSync } = await import("fs");
123
+ mkdirSync(installPath, { recursive: true });
124
+ const fsStats = statfsSync(installPath);
125
+ const freeSpace = fsStats.bsize * fsStats.bavail;
126
+ // 需要解压后约 2x 空间(压缩包 + 解压内容)
127
+ if (freeSpace < totalSize * 2) {
128
+ ui.warning(`磁盘剩余空间不足: ${ui.fileSize(freeSpace)} 可用, 预计需要 ${ui.fileSize(totalSize * 2)}`);
129
+ }
130
+ }
131
+ }
132
+ catch { /* best-effort: statfsSync 可能在某些平台不可用 */ }
113
133
  // Install each
114
134
  let successCount = 0;
115
135
  for (const id of selectedIds) {
@@ -36,7 +36,7 @@ function getAgents(server, key) {
36
36
  {
37
37
  name: "Claude Desktop",
38
38
  configPath: claudeDesktopPath,
39
- detect: () => existsSync(claudeDesktopPath) || existsSync(join(claudeDesktopPath, "..")),
39
+ detect: () => existsSync(claudeDesktopPath), // R4-FIX #4: 只检查配置文件本身,避免父目录误检
40
40
  generate: () => ({
41
41
  mcpServers: {
42
42
  "dreamlogic-skills": {
@@ -46,6 +46,8 @@ function getAgents(server, key) {
46
46
  },
47
47
  }),
48
48
  apply: (newSection) => {
49
+ // R4-FIX #5: read-then-write 存在 TOCTOU 竞态,但此为单用户 CLI 工具,
50
+ // 风险极低。如需更强保护可使用 flock,但当前场景不需要。
49
51
  let existing = {};
50
52
  if (existsSync(claudeDesktopPath)) {
51
53
  try {
@@ -108,6 +110,7 @@ function getAgents(server, key) {
108
110
  },
109
111
  }),
110
112
  apply: (newSection) => {
113
+ // R4-FIX #5: read-then-write TOCTOU — 单用户 CLI 风险极低,添加注释说明
111
114
  const path = join(home, ".cursor", "mcp.json");
112
115
  let existing = {};
113
116
  if (existsSync(path)) {
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import * as clack from "@clack/prompts";
10
10
  import { CLI_VERSION, CLI_NAME, CLI_AUTHOR } from "./types.js";
11
11
  import { ui } from "./lib/ui.js";
12
12
  import { getApiKey } from "./lib/config.js";
13
+ import boxen from "boxen";
13
14
  // Commands
14
15
  import { loginCommand } from "./commands/login.js";
15
16
  import { logoutCommand } from "./commands/logout.js";
@@ -29,8 +30,15 @@ program
29
30
  program
30
31
  .command("login")
31
32
  .description("登录 — 验证 API Key")
32
- .option("-k, --key <key>", "API Key(留空则交互输入)")
33
- .action((opts) => loginCommand(opts));
33
+ // R4-FIX #1: API Key 通过 CLI 参数暴露风险警告
34
+ .option("-k, --key <key>", "API Key(⚠️ 不推荐:会暴露在进程列表和 shell 历史中,建议用交互输入或环境变量)")
35
+ .action((opts) => {
36
+ // R4-FIX #1: 清除 process.title 中的敏感参数
37
+ if (opts.key) {
38
+ process.title = "dreamlogic login";
39
+ }
40
+ return loginCommand(opts);
41
+ });
34
42
  // ===== logout =====
35
43
  program
36
44
  .command("logout")
@@ -134,10 +142,25 @@ program.action(async () => {
134
142
  // Handle errors gracefully
135
143
  program.exitOverride();
136
144
  async function main() {
137
- // BUG-2: Warn if TLS verification is disabled
145
+ // R4-FIX #9: TLS 验证禁用 醒目安全警告(红色框)
138
146
  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
139
- ui.warning("检测到 NODE_TLS_REJECT_UNAUTHORIZED=0 — TLS 证书验证已禁用");
140
- ui.warning("存在安全风险,建议移除:set NODE_TLS_REJECT_UNAUTHORIZED=");
147
+ const tlsWarning = [
148
+ "NODE_TLS_REJECT_UNAUTHORIZED=0 已检测到",
149
+ "TLS 证书验证已完全禁用!",
150
+ "",
151
+ "风险:所有 HTTPS 请求均不验证服务器身份,",
152
+ "可能遭受中间人攻击 (MITM)。",
153
+ "",
154
+ "修复:unset NODE_TLS_REJECT_UNAUTHORIZED",
155
+ ].join("\n");
156
+ console.log(boxen(tlsWarning, {
157
+ title: "⚠️ 安全警告",
158
+ titleAlignment: "center",
159
+ padding: 1,
160
+ margin: { top: 1, bottom: 1, left: 2, right: 0 },
161
+ borderStyle: "double",
162
+ borderColor: "red",
163
+ }));
141
164
  }
142
165
  try {
143
166
  await program.parseAsync(process.argv);
@@ -156,7 +179,21 @@ async function main() {
156
179
  clack.cancel("Cancelled.");
157
180
  return;
158
181
  }
159
- ui.err(`意外错误: ${err.message}`);
182
+ // R4-FIX #10: 错误信息不泄漏内部路径/堆栈
183
+ const knownErrors = ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EACCES", "EPERM"];
184
+ const errMsg = err.message || "";
185
+ if (knownErrors.some(code => errMsg.includes(code))) {
186
+ ui.err(`操作失败: ${errMsg}`);
187
+ }
188
+ else if (errMsg.includes("SHA256") || errMsg.includes("Invalid skill ID")) {
189
+ ui.err(errMsg);
190
+ }
191
+ else {
192
+ ui.err("发生意外错误,请重试或联系支持。");
193
+ if (process.env.DREAMLOGIC_DEBUG === "1") {
194
+ ui.line(`调试信息: ${errMsg}`);
195
+ }
196
+ }
160
197
  process.exitCode = 1;
161
198
  }
162
199
  }
@@ -12,6 +12,8 @@ export declare class ApiClient {
12
12
  downloadPackage(filename: string, onProgress?: (downloaded: number, total: number) => void): Promise<{
13
13
  buffer: Buffer;
14
14
  sha256: string;
15
+ contentLength: number;
16
+ warnLargeDownload: boolean;
15
17
  }>;
16
18
  /** GET /health — check server connectivity (R2-09: validated) */
17
19
  health(): Promise<{
@@ -11,6 +11,8 @@ import { CLI_VERSION } from "../types.js";
11
11
  const API_TIMEOUT = 30_000; // 30s for API calls
12
12
  const DOWNLOAD_TIMEOUT = 300_000; // 5min for downloads
13
13
  const MAX_DOWNLOAD_SIZE = 200 * 1024 * 1024; // 200MB
14
+ // R4-FIX #3: 下载预警阈值
15
+ const DOWNLOAD_WARN_SIZE = 50 * 1024 * 1024; // 50MB
14
16
  export class ApiClient {
15
17
  baseUrl;
16
18
  apiKey;
@@ -81,6 +83,8 @@ export class ApiClient {
81
83
  if (!Number.isNaN(contentLength) && contentLength > 0 && contentLength > MAX_DOWNLOAD_SIZE) {
82
84
  throw new ApiError(`Package too large: ${contentLength} bytes (max ${MAX_DOWNLOAD_SIZE})`, 413);
83
85
  }
86
+ // R4-FIX #3: 大包下载预警 — 通过回调通知调用者
87
+ const warnLargeDownload = !Number.isNaN(contentLength) && contentLength > DOWNLOAD_WARN_SIZE;
84
88
  const reader = res.body?.getReader();
85
89
  if (!reader)
86
90
  throw new ApiError("No response body", 500);
@@ -103,7 +107,7 @@ export class ApiClient {
103
107
  }
104
108
  const buffer = Buffer.concat(chunks);
105
109
  const sha256 = createHash("sha256").update(buffer).digest("hex");
106
- return { buffer, sha256 };
110
+ return { buffer, sha256, contentLength, warnLargeDownload };
107
111
  }
108
112
  /** GET /health — check server connectivity (R2-09: validated) */
109
113
  async health() {
@@ -4,8 +4,19 @@
4
4
  */
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from "fs";
6
6
  import { join, resolve } from "path";
7
- import { homedir } from "os";
7
+ import { homedir, platform } from "os";
8
8
  import { CONFIG_DIR_NAME, DEFAULT_SERVER, DEFAULT_INSTALL_DIR_NAME, } from "../types.js";
9
+ // R4-FIX #11: Windows 不支持 Unix 文件权限,跳过 chmod
10
+ const isWin = platform() === "win32";
11
+ /** Platform-safe chmod: 只在 Unix 系统上执行 */
12
+ function safeChmod(path, mode) {
13
+ if (isWin)
14
+ return; // R4-FIX #11: Windows 下跳过
15
+ try {
16
+ chmodSync(path, mode);
17
+ }
18
+ catch { /* best-effort */ }
19
+ }
9
20
  /** CFG-01 FIX: Recursively strip prototype pollution keys from parsed JSON */
10
21
  function sanitize(obj) {
11
22
  if (typeof obj !== "object" || obj === null)
@@ -61,10 +72,7 @@ export function saveConfig(config) {
61
72
  ensureConfigDir();
62
73
  const path = getConfigPath();
63
74
  writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
64
- try {
65
- chmodSync(path, 0o600);
66
- }
67
- catch { /* Windows fallback */ }
75
+ safeChmod(path, 0o600);
68
76
  }
69
77
  /** R1-06: Validate key format from all sources (env, config file) */
70
78
  export function getApiKey() {
@@ -104,6 +112,13 @@ export function loadInstalled() {
104
112
  // R2-13: Basic shape validation
105
113
  if (typeof data !== "object" || data === null || Array.isArray(data))
106
114
  return {};
115
+ // R4-FIX #12: 兼容旧数据 — 将 number 类型的 installed_at 统一为 ISO string
116
+ for (const key of Object.keys(data)) {
117
+ const entry = data[key];
118
+ if (entry && typeof entry.installed_at === "number") {
119
+ entry.installed_at = new Date(entry.installed_at).toISOString();
120
+ }
121
+ }
107
122
  return data;
108
123
  }
109
124
  catch {
@@ -115,10 +130,7 @@ export function saveInstalled(registry) {
115
130
  ensureConfigDir();
116
131
  const path = getInstalledPath();
117
132
  writeFileSync(path, JSON.stringify(registry, null, 2) + "\n", { mode: 0o600 });
118
- try {
119
- chmodSync(path, 0o600);
120
- }
121
- catch { /* Windows fallback */ }
133
+ safeChmod(path, 0o600);
122
134
  }
123
135
  /** R1-13: Zero-fill before overwrite for defense-in-depth */
124
136
  export function clearConfig() {
@@ -54,6 +54,10 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
54
54
  buffer = result.buffer;
55
55
  actualSha256 = result.sha256;
56
56
  spinner.succeed(` 已下载 ${ui.fileSize(buffer.length)}`);
57
+ // R4-FIX #3: 大包内存占用警告
58
+ if (result.warnLargeDownload) {
59
+ ui.warning(`包体积较大 (${ui.fileSize(buffer.length)}),内存占用较高`);
60
+ }
57
61
  }
58
62
  catch (err) {
59
63
  spinner.fail(` 下载失败`);
@@ -176,6 +180,23 @@ export function rollbackSkill(skillId) {
176
180
  if (backups.length === 0)
177
181
  return false;
178
182
  const latestBackup = join(installDir, backups[0]);
183
+ // R4-FIX #6: 回滚前验证备份目录完整性
184
+ try {
185
+ const backupStat = statSync(latestBackup);
186
+ if (!backupStat.isDirectory()) {
187
+ ui.warning("备份路径不是有效目录,回滚中止");
188
+ return false;
189
+ }
190
+ const backupContents = readdirSync(latestBackup);
191
+ if (backupContents.length === 0) {
192
+ ui.warning("备份目录为空,回滚中止 — 可能备份已损坏");
193
+ return false;
194
+ }
195
+ }
196
+ catch {
197
+ ui.warning("无法验证备份完整性,回滚中止");
198
+ return false;
199
+ }
179
200
  if (existsSync(skillDir)) {
180
201
  rmSync(skillDir, { recursive: true, force: true });
181
202
  }
@@ -244,6 +265,16 @@ function extractZip(buffer, targetDir) {
244
265
  reject(new Error(`Unsafe path in ZIP: ${entry.fileName}`));
245
266
  return;
246
267
  }
268
+ // R4-FIX #2: 拒绝 Windows Alternate Data Streams (filename:stream)
269
+ // 排除 Windows 盘符 (如 C:\)
270
+ const pathParts = entryPath.split(sep);
271
+ for (const part of pathParts) {
272
+ if (part.includes(":") && !/^[A-Za-z]:$/.test(part)) {
273
+ zipfile.close();
274
+ reject(new Error(`不安全的路径(可能包含 Windows ADS): ${entry.fileName}`));
275
+ return;
276
+ }
277
+ }
247
278
  // INS-02 FIX: Reject symlink entries (external attributes bit 0xA000)
248
279
  const externalAttrs = (entry.externalFileAttributes >>> 16) & 0xFFFF;
249
280
  const S_IFLNK = 0xA000;
package/dist/types.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface CliConfig {
22
22
  }
23
23
  export interface InstalledSkill {
24
24
  version: string;
25
+ /** R4-FIX #12: 统一为 ISO 8601 字符串 (如 "2025-01-01T00:00:00.000Z") */
25
26
  installed_at: string;
26
27
  path: string;
27
28
  sha256: string;
@@ -33,6 +34,6 @@ export interface InstalledRegistry {
33
34
  export declare const DEFAULT_SERVER = "https://skill.dreamlogic-claw.com";
34
35
  export declare const DEFAULT_INSTALL_DIR_NAME = "dreamlogic-skills";
35
36
  export declare const CONFIG_DIR_NAME = ".dreamlogic";
36
- export declare const CLI_VERSION = "2.0.5";
37
+ export declare const CLI_VERSION = "2.0.6";
37
38
  export declare const CLI_NAME = "Dreamlogic CLI";
38
39
  export declare const CLI_AUTHOR = "Dreamlogic-ai by MAJORNINE";
package/dist/types.js CHANGED
@@ -2,6 +2,6 @@
2
2
  export const DEFAULT_SERVER = "https://skill.dreamlogic-claw.com";
3
3
  export const DEFAULT_INSTALL_DIR_NAME = "dreamlogic-skills";
4
4
  export const CONFIG_DIR_NAME = ".dreamlogic";
5
- export const CLI_VERSION = "2.0.5";
5
+ export const CLI_VERSION = "2.0.6";
6
6
  export const CLI_NAME = "Dreamlogic CLI";
7
7
  export const CLI_AUTHOR = "Dreamlogic-ai by MAJORNINE";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreamlogic-ai/cli",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "Dreamlogic AI Skill Manager — Install, update and manage AI agent skills",
5
5
  "type": "module",
6
6
  "bin": {