@dreamlogic-ai/cli 2.0.2 → 2.0.4
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/dist/commands/catalog.js +4 -4
- package/dist/commands/helpers.js +1 -1
- package/dist/commands/install.js +26 -26
- package/dist/commands/list.js +7 -7
- package/dist/commands/login.js +16 -16
- package/dist/commands/logout.js +4 -4
- package/dist/commands/rollback.js +12 -12
- package/dist/commands/setup-mcp.js +18 -18
- package/dist/commands/status.js +8 -8
- package/dist/commands/update.js +18 -18
- package/dist/index.js +27 -27
- package/dist/lib/agents.js +14 -1
- package/dist/lib/config.js +13 -10
- package/dist/lib/installer.js +20 -15
- package/dist/lib/ui.d.ts +2 -2
- package/dist/lib/ui.js +15 -6
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
package/dist/commands/catalog.js
CHANGED
|
@@ -10,14 +10,14 @@ export async function catalogCommand() {
|
|
|
10
10
|
if (!apiKey)
|
|
11
11
|
return;
|
|
12
12
|
const client = new ApiClient(getServer(), apiKey);
|
|
13
|
-
const spinner = ui.spinner("
|
|
13
|
+
const spinner = ui.spinner("正在获取技能目录...");
|
|
14
14
|
spinner.start();
|
|
15
15
|
try {
|
|
16
16
|
const skills = await client.listSkills();
|
|
17
|
-
spinner.succeed(` ${skills.length}
|
|
17
|
+
spinner.succeed(` 共 ${skills.length} 个技能可用`);
|
|
18
18
|
console.log();
|
|
19
19
|
if (skills.length === 0) {
|
|
20
|
-
ui.info("
|
|
20
|
+
ui.info("暂无可用技能,请稍后再试!");
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
for (const s of skills) {
|
|
@@ -32,7 +32,7 @@ export async function catalogCommand() {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
catch (err) {
|
|
35
|
-
spinner.fail("
|
|
35
|
+
spinner.fail(" 获取目录失败");
|
|
36
36
|
ui.err(err.message);
|
|
37
37
|
process.exitCode = 1;
|
|
38
38
|
}
|
package/dist/commands/helpers.js
CHANGED
package/dist/commands/install.js
CHANGED
|
@@ -17,15 +17,15 @@ export async function installCommand(skillIds, opts) {
|
|
|
17
17
|
return;
|
|
18
18
|
const client = new ApiClient(getServer(), apiKey);
|
|
19
19
|
// Fetch catalog
|
|
20
|
-
const spinner = ui.spinner("
|
|
20
|
+
const spinner = ui.spinner("正在获取可用技能...");
|
|
21
21
|
spinner.start();
|
|
22
22
|
let skills;
|
|
23
23
|
try {
|
|
24
24
|
skills = await client.listSkills();
|
|
25
|
-
spinner.succeed(` ${skills.length}
|
|
25
|
+
spinner.succeed(` 共 ${skills.length} 个技能可用`);
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
28
|
-
spinner.fail("
|
|
28
|
+
spinner.fail(" 连接失败");
|
|
29
29
|
ui.err(err.message);
|
|
30
30
|
process.exitCode = 1;
|
|
31
31
|
return;
|
|
@@ -35,7 +35,7 @@ export async function installCommand(skillIds, opts) {
|
|
|
35
35
|
let selectedIds;
|
|
36
36
|
if (skillIds.length === 0) {
|
|
37
37
|
if (skills.length === 0) {
|
|
38
|
-
ui.info("
|
|
38
|
+
ui.info("暂无可用技能。");
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
const options = skills.map((s) => {
|
|
@@ -44,13 +44,13 @@ export async function installCommand(skillIds, opts) {
|
|
|
44
44
|
const currentVer = installed[s.id]?.version;
|
|
45
45
|
let hint = "";
|
|
46
46
|
if (!isInstalled) {
|
|
47
|
-
hint = chalk.green("
|
|
47
|
+
hint = chalk.green("全新");
|
|
48
48
|
}
|
|
49
49
|
else if (currentVer && s.latest_version && currentVer !== s.latest_version) {
|
|
50
|
-
hint = chalk.yellow(
|
|
50
|
+
hint = chalk.yellow(`可更新: ${currentVer} → ${s.latest_version}`);
|
|
51
51
|
}
|
|
52
52
|
else {
|
|
53
|
-
hint = chalk.dim("
|
|
53
|
+
hint = chalk.dim("已安装");
|
|
54
54
|
}
|
|
55
55
|
return {
|
|
56
56
|
label: `${s.name} (${ver})`,
|
|
@@ -60,17 +60,17 @@ export async function installCommand(skillIds, opts) {
|
|
|
60
60
|
});
|
|
61
61
|
console.log();
|
|
62
62
|
const selected = await clack.multiselect({
|
|
63
|
-
message: "
|
|
63
|
+
message: "选择要安装的技能(空格选择,回车确认):",
|
|
64
64
|
options,
|
|
65
65
|
required: true,
|
|
66
66
|
});
|
|
67
67
|
if (clack.isCancel(selected)) {
|
|
68
|
-
clack.cancel("
|
|
68
|
+
clack.cancel("安装已取消。");
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
selectedIds = selected;
|
|
72
72
|
if (selectedIds.length === 0) {
|
|
73
|
-
ui.info("
|
|
73
|
+
ui.info("未选择任何技能。");
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -78,8 +78,8 @@ export async function installCommand(skillIds, opts) {
|
|
|
78
78
|
// Validate requested skill IDs
|
|
79
79
|
const invalid = skillIds.filter((id) => !skills.find((s) => s.id === id));
|
|
80
80
|
if (invalid.length > 0) {
|
|
81
|
-
ui.err(
|
|
82
|
-
ui.info("
|
|
81
|
+
ui.err(`未知技能: ${invalid.join(", ")}`);
|
|
82
|
+
ui.info("可用技能: " + skills.map((s) => s.id).join(", "));
|
|
83
83
|
process.exitCode = 1;
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
@@ -87,7 +87,7 @@ export async function installCommand(skillIds, opts) {
|
|
|
87
87
|
}
|
|
88
88
|
// R1-12: --from-file only works with single skill
|
|
89
89
|
if (opts.fromFile && selectedIds.length > 1) {
|
|
90
|
-
ui.err("--from-file
|
|
90
|
+
ui.err("--from-file 仅支持单个技能安装。");
|
|
91
91
|
process.exitCode = 1;
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
@@ -102,11 +102,11 @@ export async function installCommand(skillIds, opts) {
|
|
|
102
102
|
return `${chalk.bold(s.name)} ${chalk.dim(ver)} (${size})`;
|
|
103
103
|
});
|
|
104
104
|
planLines.push("");
|
|
105
|
-
planLines.push(`${chalk.dim("
|
|
106
|
-
ui.panel("
|
|
107
|
-
const ok = await clack.confirm({ message: "
|
|
105
|
+
planLines.push(`${chalk.dim("安装目录:")} ${getInstallDir()}`);
|
|
106
|
+
ui.panel("安装计划", planLines.join("\n"));
|
|
107
|
+
const ok = await clack.confirm({ message: "确认安装?", initialValue: true });
|
|
108
108
|
if (clack.isCancel(ok) || !ok) {
|
|
109
|
-
clack.cancel("
|
|
109
|
+
clack.cancel("已取消。");
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -115,26 +115,26 @@ export async function installCommand(skillIds, opts) {
|
|
|
115
115
|
for (const id of selectedIds) {
|
|
116
116
|
const s = skills.find((s) => s.id === id);
|
|
117
117
|
if (!s) {
|
|
118
|
-
ui.err(
|
|
118
|
+
ui.err(`技能 '${id}' 已不在目录中`);
|
|
119
119
|
continue;
|
|
120
120
|
}
|
|
121
121
|
console.log();
|
|
122
|
-
ui.header(
|
|
122
|
+
ui.header(`正在安装 ${s.name}`);
|
|
123
123
|
try {
|
|
124
124
|
const result = await installSkill(client, s.id, s.package_file || `${s.id}-${s.latest_version}.zip`, s.package_sha256, s.latest_version || "unknown", { fromFile: opts.fromFile });
|
|
125
|
-
ui.ok(
|
|
125
|
+
ui.ok(`已安装到 ${result.path}`);
|
|
126
126
|
successCount++;
|
|
127
127
|
}
|
|
128
128
|
catch (err) {
|
|
129
|
-
ui.err(
|
|
129
|
+
ui.err(`安装 ${s.name} 失败: ${err.message}`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
console.log();
|
|
133
133
|
if (successCount === selectedIds.length) {
|
|
134
|
-
ui.ok(
|
|
134
|
+
ui.ok(`全部 ${successCount} 个技能安装成功!`);
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
137
|
-
ui.warning(`${successCount}/${selectedIds.length}
|
|
137
|
+
ui.warning(`${successCount}/${selectedIds.length} 已安装,请检查上方错误信息。`);
|
|
138
138
|
}
|
|
139
139
|
// ── Agent Registration Step ──
|
|
140
140
|
if (successCount > 0 && !opts.yes) {
|
|
@@ -161,7 +161,7 @@ export async function installCommand(skillIds, opts) {
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
const agentChoice = await clack.multiselect({
|
|
164
|
-
message:
|
|
164
|
+
message: `注册技能到 AI Agent?(共 ${totalAgents} 个可用)`,
|
|
165
165
|
options: agentOptions,
|
|
166
166
|
required: false,
|
|
167
167
|
});
|
|
@@ -177,7 +177,7 @@ export async function installCommand(skillIds, opts) {
|
|
|
177
177
|
const created = results.filter(r => r.status === "created").length;
|
|
178
178
|
const errors = results.filter(r => r.status === "error");
|
|
179
179
|
if (created > 0) {
|
|
180
|
-
ui.ok(`${id}:
|
|
180
|
+
ui.ok(`${id}: 已注册到 ${created} 个 Agent`);
|
|
181
181
|
}
|
|
182
182
|
for (const e of errors) {
|
|
183
183
|
ui.warning(`${e.agent}: ${e.error}`);
|
|
@@ -189,7 +189,7 @@ export async function installCommand(skillIds, opts) {
|
|
|
189
189
|
// Suggest MCP setup
|
|
190
190
|
if (successCount > 0) {
|
|
191
191
|
console.log();
|
|
192
|
-
ui.info("
|
|
192
|
+
ui.info("如需配置 MCP 连接 AI Agent,请运行:");
|
|
193
193
|
ui.line(chalk.cyan("dreamlogic setup-mcp"));
|
|
194
194
|
}
|
|
195
195
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -8,24 +8,24 @@ export function listCommand() {
|
|
|
8
8
|
const installed = loadInstalled();
|
|
9
9
|
const entries = Object.entries(installed);
|
|
10
10
|
if (entries.length === 0) {
|
|
11
|
-
ui.info("
|
|
11
|
+
ui.info("暂无已安装的技能。请运行: dreamlogic install");
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
console.log();
|
|
15
|
-
ui.line(
|
|
15
|
+
ui.line(`安装目录: ${getInstallDir()}`);
|
|
16
16
|
console.log();
|
|
17
17
|
for (const [id, info] of entries) {
|
|
18
18
|
ui.line(` ${chalk.green("●")} ${chalk.bold(id)}`);
|
|
19
19
|
ui.table([
|
|
20
|
-
["
|
|
21
|
-
["
|
|
22
|
-
["
|
|
20
|
+
["版本", info.version],
|
|
21
|
+
["安装时间", new Date(info.installed_at).toLocaleString()],
|
|
22
|
+
["路径", info.path],
|
|
23
23
|
["SHA256", info.sha256.slice(0, 16) + "..."],
|
|
24
24
|
]);
|
|
25
25
|
if (info.previous_version) {
|
|
26
|
-
ui.line(` ${ui.dim(
|
|
26
|
+
ui.line(` ${ui.dim(`上一版本: ${info.previous_version}`)}`);
|
|
27
27
|
}
|
|
28
28
|
console.log();
|
|
29
29
|
}
|
|
30
|
-
ui.line(`
|
|
30
|
+
ui.line(` 共计: ${entries.length} 个技能`);
|
|
31
31
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -10,9 +10,9 @@ const KEY_RE = /^sk-(admin|user)-[a-f0-9]{16,}$/;
|
|
|
10
10
|
export async function loginCommand(opts) {
|
|
11
11
|
const existingKey = getApiKey();
|
|
12
12
|
if (existingKey && !opts.key) {
|
|
13
|
-
ui.info(
|
|
13
|
+
ui.info(`当前已登录: ${maskKey(existingKey)}`);
|
|
14
14
|
const relogin = await clack.confirm({
|
|
15
|
-
message: "
|
|
15
|
+
message: "是否切换到其他 Key?",
|
|
16
16
|
initialValue: false,
|
|
17
17
|
});
|
|
18
18
|
if (clack.isCancel(relogin) || !relogin)
|
|
@@ -21,57 +21,57 @@ export async function loginCommand(opts) {
|
|
|
21
21
|
let apiKey = opts.key || "";
|
|
22
22
|
// R1-04: Warn about key visibility in args/history
|
|
23
23
|
if (opts.key) {
|
|
24
|
-
ui.warning("Key
|
|
25
|
-
ui.info("
|
|
24
|
+
ui.warning("Key 通过命令行参数传入 — 可能会记录在终端历史中");
|
|
25
|
+
ui.info("建议使用交互模式: dreamlogic login 或环境变量 DREAMLOGIC_API_KEY");
|
|
26
26
|
}
|
|
27
27
|
if (!apiKey) {
|
|
28
28
|
const keyInput = await clack.password({
|
|
29
|
-
message: "
|
|
29
|
+
message: "请输入你的 Dreamlogic API Key:",
|
|
30
30
|
mask: "*",
|
|
31
31
|
validate: (v) => {
|
|
32
32
|
if (!v || !KEY_RE.test(v))
|
|
33
|
-
return "Key
|
|
33
|
+
return "Key 格式应为: sk-user-xxxx... 或 sk-admin-xxxx...";
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
if (clack.isCancel(keyInput)) {
|
|
37
|
-
clack.cancel("
|
|
37
|
+
clack.cancel("登录已取消");
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
apiKey = keyInput;
|
|
41
41
|
}
|
|
42
42
|
if (!KEY_RE.test(apiKey)) {
|
|
43
|
-
ui.err("
|
|
43
|
+
ui.err("Key 格式不正确,应为: sk-user-xxxx... 或 sk-admin-xxxx...");
|
|
44
44
|
process.exitCode = 1;
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
const server = getServer();
|
|
48
|
-
const spinner = ui.spinner("
|
|
48
|
+
const spinner = ui.spinner("正在验证 Key...");
|
|
49
49
|
spinner.start();
|
|
50
50
|
try {
|
|
51
51
|
const client = new ApiClient(server, apiKey);
|
|
52
52
|
const user = await client.me();
|
|
53
|
-
spinner.succeed(`
|
|
53
|
+
spinner.succeed(` 欢迎回来, ${ui.brand(user.name)}! (${user.role})`);
|
|
54
54
|
const config = loadConfig() || { api_key: "", server: DEFAULT_SERVER, install_dir: getDefaultInstallDir() };
|
|
55
55
|
config.api_key = apiKey;
|
|
56
56
|
config.server = server;
|
|
57
57
|
if (!config.install_dir)
|
|
58
58
|
config.install_dir = getDefaultInstallDir();
|
|
59
59
|
saveConfig(config);
|
|
60
|
-
ui.ok("
|
|
60
|
+
ui.ok("配置已保存到 ~/.dreamlogic/config.json");
|
|
61
61
|
}
|
|
62
62
|
catch (err) {
|
|
63
|
-
spinner.fail("
|
|
63
|
+
spinner.fail(" 认证失败");
|
|
64
64
|
if (err instanceof ApiError) {
|
|
65
65
|
if (err.status === 401) {
|
|
66
|
-
ui.err("
|
|
66
|
+
ui.err("无效或已禁用的 API Key,请检查后重试。");
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
|
-
ui.err(
|
|
69
|
+
ui.err(`服务器错误: ${err.message}`);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
|
-
ui.err(
|
|
74
|
-
ui.info(
|
|
73
|
+
ui.err(`连接失败: ${err.message}`);
|
|
74
|
+
ui.info(`服务器地址: ${server}`);
|
|
75
75
|
}
|
|
76
76
|
process.exitCode = 1;
|
|
77
77
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -7,13 +7,13 @@ import { ui } from "../lib/ui.js";
|
|
|
7
7
|
export async function logoutCommand() {
|
|
8
8
|
const key = getApiKey();
|
|
9
9
|
if (!key) {
|
|
10
|
-
ui.info("
|
|
10
|
+
ui.info("当前未登录。");
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
ui.info(
|
|
14
|
-
const ok = await clack.confirm({ message: "
|
|
13
|
+
ui.info(`当前 Key: ${maskKey(key)}`);
|
|
14
|
+
const ok = await clack.confirm({ message: "确定清除已保存的凭证?", initialValue: true });
|
|
15
15
|
if (clack.isCancel(ok) || !ok)
|
|
16
16
|
return;
|
|
17
17
|
clearConfig();
|
|
18
|
-
ui.ok("
|
|
18
|
+
ui.ok("凭证已清除。");
|
|
19
19
|
}
|
|
@@ -8,33 +8,33 @@ import { ui } from "../lib/ui.js";
|
|
|
8
8
|
export async function rollbackCommand(skillId) {
|
|
9
9
|
const installed = loadInstalled();
|
|
10
10
|
if (!installed[skillId]) {
|
|
11
|
-
ui.err(
|
|
11
|
+
ui.err(`技能 '${skillId}' 未安装。`);
|
|
12
12
|
process.exitCode = 1;
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
const info = installed[skillId];
|
|
16
16
|
if (!info.previous_version) {
|
|
17
|
-
ui.err(
|
|
18
|
-
ui.info(
|
|
17
|
+
ui.err(`没有可用的历史版本进行回滚。`);
|
|
18
|
+
ui.info(`当前版本: ${info.version}`);
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
ui.info(
|
|
22
|
-
ui.info(
|
|
23
|
-
const ok = await clack.confirm({ message: "
|
|
21
|
+
ui.info(`当前版本: ${info.version}`);
|
|
22
|
+
ui.info(`回滚到: ${info.previous_version}`);
|
|
23
|
+
const ok = await clack.confirm({ message: "确认回滚?", initialValue: true });
|
|
24
24
|
if (clack.isCancel(ok) || !ok) {
|
|
25
|
-
clack.cancel("
|
|
25
|
+
clack.cancel("已取消。");
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
const spinner = ui.spinner("
|
|
28
|
+
const spinner = ui.spinner("正在回滚...");
|
|
29
29
|
spinner.start();
|
|
30
30
|
const success = rollbackSkill(skillId);
|
|
31
31
|
if (success) {
|
|
32
|
-
spinner.succeed(`
|
|
33
|
-
ui.ok("
|
|
32
|
+
spinner.succeed(` 已回滚到上一个版本`);
|
|
33
|
+
ui.ok("回滚完成。");
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
|
-
spinner.fail("
|
|
37
|
-
ui.err("
|
|
36
|
+
spinner.fail(" 回滚失败");
|
|
37
|
+
ui.err("未找到备份,需要手动恢复。");
|
|
38
38
|
process.exitCode = 1;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -54,7 +54,7 @@ function getAgents(server, key) {
|
|
|
54
54
|
catch {
|
|
55
55
|
// R2-07: Backup corrupted config instead of silently discarding
|
|
56
56
|
const bakPath = claudeDesktopPath + ".bak";
|
|
57
|
-
writeFileSync(bakPath, readFileSync(claudeDesktopPath));
|
|
57
|
+
writeFileSync(bakPath, readFileSync(claudeDesktopPath), { mode: 0o600 });
|
|
58
58
|
ui.warning(`Existing config has invalid JSON — backed up to ${bakPath}`);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -117,7 +117,7 @@ function getAgents(server, key) {
|
|
|
117
117
|
catch {
|
|
118
118
|
// R2-07: Backup corrupted config
|
|
119
119
|
const bakPath = path + ".bak";
|
|
120
|
-
writeFileSync(bakPath, readFileSync(path));
|
|
120
|
+
writeFileSync(bakPath, readFileSync(path), { mode: 0o600 });
|
|
121
121
|
ui.warning(`Existing config has invalid JSON — backed up to ${bakPath}`);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -140,31 +140,31 @@ export async function setupMcpCommand(opts) {
|
|
|
140
140
|
const server = getServer();
|
|
141
141
|
const agents = getAgents(server, apiKey);
|
|
142
142
|
console.log();
|
|
143
|
-
ui.header("MCP Agent
|
|
143
|
+
ui.header("MCP Agent 配置");
|
|
144
144
|
console.log();
|
|
145
145
|
// Detect installed agents
|
|
146
146
|
const detected = agents.filter((a) => a.detect());
|
|
147
147
|
const notDetected = agents.filter((a) => !a.detect());
|
|
148
148
|
if (detected.length > 0) {
|
|
149
|
-
ui.ok(
|
|
149
|
+
ui.ok(`检测到 ${detected.length} 个 Agent:`);
|
|
150
150
|
for (const a of detected)
|
|
151
151
|
ui.line(` ${chalk.green("●")} ${a.name}`);
|
|
152
152
|
}
|
|
153
153
|
if (notDetected.length > 0) {
|
|
154
|
-
ui.line(` ${ui.dim("
|
|
154
|
+
ui.line(` ${ui.dim("未检测到: " + notDetected.map((a) => a.name).join(", "))}`);
|
|
155
155
|
}
|
|
156
156
|
if (detected.length === 0) {
|
|
157
|
-
ui.warning("
|
|
158
|
-
ui.info("
|
|
157
|
+
ui.warning("未检测到支持的 Agent。");
|
|
158
|
+
ui.info("支持的 Agent: " + agents.map((a) => a.name).join(", "));
|
|
159
159
|
console.log();
|
|
160
|
-
ui.info("
|
|
161
|
-
ui.line(`
|
|
160
|
+
ui.info("手动配置 MCP:");
|
|
161
|
+
ui.line(` 服务器 URL: ${server}/sse`);
|
|
162
162
|
ui.line(` API Key: ${maskKey(apiKey)}`);
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
|
-
// R1-01: Warn about key in MCP
|
|
166
|
-
ui.warning("MCP
|
|
167
|
-
ui.line(`
|
|
165
|
+
// R1-01: Warn about key in MCP config (inherent to MCP protocol)
|
|
166
|
+
ui.warning("MCP 配置将包含你的 API Key(MCP 协议需要认证)");
|
|
167
|
+
ui.line(` 配置文件将以安全权限写入(仅所有者可读写)。`);
|
|
168
168
|
// Configure each detected agent
|
|
169
169
|
for (const agent of detected) {
|
|
170
170
|
console.log();
|
|
@@ -180,26 +180,26 @@ export async function setupMcpCommand(opts) {
|
|
|
180
180
|
ui.line(` ${chalk.dim(maskConfigJson(config, apiKey).split("\n").join("\n "))}`);
|
|
181
181
|
}
|
|
182
182
|
if (opts.dryRun) {
|
|
183
|
-
ui.info("
|
|
183
|
+
ui.info("(预览模式 — 未做任何更改)");
|
|
184
184
|
continue;
|
|
185
185
|
}
|
|
186
186
|
const ok = await clack.confirm({
|
|
187
|
-
message:
|
|
187
|
+
message: `为 ${agent.name} 应用配置?`,
|
|
188
188
|
initialValue: true,
|
|
189
189
|
});
|
|
190
190
|
if (clack.isCancel(ok) || !ok) {
|
|
191
|
-
ui.info("
|
|
191
|
+
ui.info("已跳过。");
|
|
192
192
|
continue;
|
|
193
193
|
}
|
|
194
194
|
try {
|
|
195
195
|
agent.apply(config);
|
|
196
|
-
ui.ok(`${agent.name}
|
|
196
|
+
ui.ok(`${agent.name} 已配置`);
|
|
197
197
|
if (agent.name === "Claude Desktop") {
|
|
198
|
-
ui.info("
|
|
198
|
+
ui.info("请重启 Claude Desktop 以生效。");
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
catch (err) {
|
|
202
|
-
ui.err(
|
|
202
|
+
ui.err(`配置失败: ${err.message}`);
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
ui.goodbye();
|
package/dist/commands/status.js
CHANGED
|
@@ -10,14 +10,14 @@ export async function statusCommand() {
|
|
|
10
10
|
const config = loadConfig();
|
|
11
11
|
const installed = loadInstalled();
|
|
12
12
|
console.log();
|
|
13
|
-
ui.header("Dreamlogic CLI
|
|
13
|
+
ui.header("Dreamlogic CLI 状态总览");
|
|
14
14
|
console.log();
|
|
15
15
|
// Config
|
|
16
16
|
ui.table([
|
|
17
|
-
["CLI
|
|
18
|
-
["
|
|
19
|
-
["API Key", config?.api_key ? maskKey(config.api_key) : chalk.red("(
|
|
20
|
-
["
|
|
17
|
+
["CLI 版本", CLI_VERSION],
|
|
18
|
+
["服务器", config?.server || "(未配置)"],
|
|
19
|
+
["API Key", config?.api_key ? maskKey(config.api_key) : chalk.red("(未设置)")],
|
|
20
|
+
["安装目录", getInstallDir()],
|
|
21
21
|
]);
|
|
22
22
|
// Server health — R2-05: use redirect:"error" like all other fetches
|
|
23
23
|
const server = getServer();
|
|
@@ -33,16 +33,16 @@ export async function statusCommand() {
|
|
|
33
33
|
throw new Error("Invalid health response");
|
|
34
34
|
}
|
|
35
35
|
console.log();
|
|
36
|
-
ui.ok(
|
|
36
|
+
ui.ok(`服务器: ${data.status} (v${data.version})`);
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
39
39
|
console.log();
|
|
40
|
-
ui.warning("
|
|
40
|
+
ui.warning("服务器不可达");
|
|
41
41
|
}
|
|
42
42
|
// Installed skills
|
|
43
43
|
const entries = Object.entries(installed);
|
|
44
44
|
console.log();
|
|
45
|
-
ui.line(
|
|
45
|
+
ui.line(`已安装: ${entries.length} 个技能`);
|
|
46
46
|
if (entries.length > 0) {
|
|
47
47
|
// Check for updates if authenticated
|
|
48
48
|
const apiKey = getApiKey();
|
package/dist/commands/update.js
CHANGED
|
@@ -15,11 +15,11 @@ export async function updateCommand(opts) {
|
|
|
15
15
|
const installed = loadInstalled();
|
|
16
16
|
const installedIds = Object.keys(installed);
|
|
17
17
|
if (installedIds.length === 0) {
|
|
18
|
-
ui.info("
|
|
18
|
+
ui.info("暂无已安装的技能。请运行: dreamlogic install");
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
const client = new ApiClient(getServer(), apiKey);
|
|
22
|
-
const spinner = ui.spinner("
|
|
22
|
+
const spinner = ui.spinner("正在检查更新...");
|
|
23
23
|
spinner.start();
|
|
24
24
|
let skills;
|
|
25
25
|
try {
|
|
@@ -27,7 +27,7 @@ export async function updateCommand(opts) {
|
|
|
27
27
|
spinner.stop();
|
|
28
28
|
}
|
|
29
29
|
catch (err) {
|
|
30
|
-
spinner.fail("
|
|
30
|
+
spinner.fail(" 检查更新失败");
|
|
31
31
|
ui.err(err.message);
|
|
32
32
|
process.exitCode = 1;
|
|
33
33
|
return;
|
|
@@ -39,16 +39,16 @@ export async function updateCommand(opts) {
|
|
|
39
39
|
const local = installed[id];
|
|
40
40
|
const remote = skills.find((s) => s.id === id);
|
|
41
41
|
if (!remote) {
|
|
42
|
-
ui.line(`${ui.warn("?")} ${id} —
|
|
42
|
+
ui.line(`${ui.warn("?")} ${id} — 服务器上未找到(已移除?)`);
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
if (!remote.latest_version) {
|
|
46
|
-
ui.line(`${ui.muted("–")} ${remote.name} —
|
|
46
|
+
ui.line(`${ui.muted("–")} ${remote.name} — 无版本信息`);
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
49
|
const normalizeVer = (v) => v.replace(/^v/, "");
|
|
50
50
|
if (normalizeVer(local.version) === normalizeVer(remote.latest_version)) {
|
|
51
|
-
ui.line(`${chalk.green("✓")} ${remote.name} ${local.version} —
|
|
51
|
+
ui.line(`${chalk.green("✓")} ${remote.name} ${local.version} — 已是最新`);
|
|
52
52
|
}
|
|
53
53
|
else {
|
|
54
54
|
ui.line(`${chalk.yellow("⬆")} ${chalk.bold(remote.name)} ${local.version} → ${chalk.green(remote.latest_version)}`);
|
|
@@ -65,11 +65,11 @@ export async function updateCommand(opts) {
|
|
|
65
65
|
}
|
|
66
66
|
if (updates.length === 0) {
|
|
67
67
|
console.log();
|
|
68
|
-
ui.ok("
|
|
68
|
+
ui.ok("所有技能均已是最新版!");
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
console.log();
|
|
72
|
-
ui.info(
|
|
72
|
+
ui.info(`发现 ${updates.length} 个可更新`);
|
|
73
73
|
// Select which to update
|
|
74
74
|
let selectedIds;
|
|
75
75
|
if (opts.yes) {
|
|
@@ -77,14 +77,14 @@ export async function updateCommand(opts) {
|
|
|
77
77
|
}
|
|
78
78
|
else if (updates.length === 1) {
|
|
79
79
|
const ok = await clack.confirm({
|
|
80
|
-
message:
|
|
80
|
+
message: `更新 ${updates[0].name} 到 ${updates[0].latestVersion}?`,
|
|
81
81
|
initialValue: true,
|
|
82
82
|
});
|
|
83
83
|
selectedIds = (clack.isCancel(ok) || !ok) ? [] : [updates[0].id];
|
|
84
84
|
}
|
|
85
85
|
else {
|
|
86
86
|
const selected = await clack.multiselect({
|
|
87
|
-
message: "
|
|
87
|
+
message: "选择要更新的技能:",
|
|
88
88
|
options: updates.map((u) => ({
|
|
89
89
|
label: `${u.name} ${u.currentVersion} → ${u.latestVersion}${u.size ? ` (${ui.fileSize(u.size)})` : ""}`,
|
|
90
90
|
value: u.id,
|
|
@@ -92,13 +92,13 @@ export async function updateCommand(opts) {
|
|
|
92
92
|
required: true,
|
|
93
93
|
});
|
|
94
94
|
if (clack.isCancel(selected)) {
|
|
95
|
-
clack.cancel("
|
|
95
|
+
clack.cancel("已取消。");
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
selectedIds = selected;
|
|
99
99
|
}
|
|
100
100
|
if (selectedIds.length === 0) {
|
|
101
|
-
ui.info("
|
|
101
|
+
ui.info("未选择任何更新。");
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
// Apply updates
|
|
@@ -108,23 +108,23 @@ export async function updateCommand(opts) {
|
|
|
108
108
|
if (!u)
|
|
109
109
|
continue;
|
|
110
110
|
console.log();
|
|
111
|
-
ui.header(
|
|
111
|
+
ui.header(`正在更新 ${u.name}`);
|
|
112
112
|
ui.line(`${u.currentVersion} → ${chalk.green(u.latestVersion)}`);
|
|
113
113
|
try {
|
|
114
114
|
await installSkill(client, u.id, u.packageFile, u.sha256, u.latestVersion);
|
|
115
|
-
ui.ok(
|
|
115
|
+
ui.ok(`已更新到 ${u.latestVersion}`);
|
|
116
116
|
successCount++;
|
|
117
117
|
}
|
|
118
118
|
catch (err) {
|
|
119
|
-
ui.err(
|
|
120
|
-
ui.info(
|
|
119
|
+
ui.err(`更新失败: ${err.message}`);
|
|
120
|
+
ui.info(`可回滚: dreamlogic rollback ${u.id}`);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
console.log();
|
|
124
124
|
if (successCount === selectedIds.length) {
|
|
125
|
-
ui.ok(
|
|
125
|
+
ui.ok(`全部 ${successCount} 个更新已应用!`);
|
|
126
126
|
}
|
|
127
127
|
else {
|
|
128
|
-
ui.warning(`${successCount}/${selectedIds.length}
|
|
128
|
+
ui.warning(`${successCount}/${selectedIds.length} 已更新,请检查上方错误信息。`);
|
|
129
129
|
}
|
|
130
130
|
}
|
package/dist/index.js
CHANGED
|
@@ -23,65 +23,65 @@ import { setupMcpCommand } from "./commands/setup-mcp.js";
|
|
|
23
23
|
const program = new Command();
|
|
24
24
|
program
|
|
25
25
|
.name("dreamlogic")
|
|
26
|
-
.description(`${CLI_NAME} — AI
|
|
26
|
+
.description(`${CLI_NAME} — AI 技能管理器\n${CLI_AUTHOR}`)
|
|
27
27
|
.version(CLI_VERSION, "-v, --version");
|
|
28
28
|
// ===== login =====
|
|
29
29
|
program
|
|
30
30
|
.command("login")
|
|
31
|
-
.description("
|
|
32
|
-
.option("-k, --key <key>", "API
|
|
31
|
+
.description("登录 — 验证 API Key")
|
|
32
|
+
.option("-k, --key <key>", "API Key(留空则交互输入)")
|
|
33
33
|
.action((opts) => loginCommand(opts));
|
|
34
34
|
// ===== logout =====
|
|
35
35
|
program
|
|
36
36
|
.command("logout")
|
|
37
|
-
.description("
|
|
37
|
+
.description("登出 — 清除已保存的凭证")
|
|
38
38
|
.action(() => logoutCommand());
|
|
39
39
|
// ===== catalog =====
|
|
40
40
|
program
|
|
41
41
|
.command("catalog")
|
|
42
|
-
.description("
|
|
42
|
+
.description("浏览 — 查看服务器上可用的技能")
|
|
43
43
|
.action(() => catalogCommand());
|
|
44
44
|
// ===== install =====
|
|
45
45
|
program
|
|
46
46
|
.command("install [skills...]")
|
|
47
|
-
.description("
|
|
48
|
-
.option("-y, --yes", "
|
|
49
|
-
.option("--from-file <path>", "
|
|
47
|
+
.description("安装 — 下载并安装技能(留空则交互选择)")
|
|
48
|
+
.option("-y, --yes", "跳过确认提示")
|
|
49
|
+
.option("--from-file <path>", "从本地 ZIP 文件安装")
|
|
50
50
|
.action((skills, opts) => installCommand(skills, opts));
|
|
51
51
|
// ===== update =====
|
|
52
52
|
program
|
|
53
53
|
.command("update")
|
|
54
|
-
.description("
|
|
55
|
-
.option("-y, --yes", "
|
|
54
|
+
.description("更新 — 检查并应用技能升级")
|
|
55
|
+
.option("-y, --yes", "自动应用全部更新")
|
|
56
56
|
.action((opts) => updateCommand(opts));
|
|
57
57
|
// ===== list =====
|
|
58
58
|
program
|
|
59
59
|
.command("list")
|
|
60
60
|
.alias("ls")
|
|
61
|
-
.description("
|
|
61
|
+
.description("列表 — 查看已安装的技能")
|
|
62
62
|
.action(() => listCommand());
|
|
63
63
|
// ===== rollback =====
|
|
64
64
|
program
|
|
65
65
|
.command("rollback <skill>")
|
|
66
|
-
.description("
|
|
66
|
+
.description("回滚 — 恢复技能到上一个版本")
|
|
67
67
|
.action((skill) => rollbackCommand(skill));
|
|
68
68
|
// ===== status =====
|
|
69
69
|
program
|
|
70
70
|
.command("status")
|
|
71
|
-
.description("
|
|
71
|
+
.description("状态 — 查看完整状态概览")
|
|
72
72
|
.action(() => statusCommand());
|
|
73
73
|
// ===== setup-mcp =====
|
|
74
74
|
program
|
|
75
75
|
.command("setup-mcp")
|
|
76
|
-
.description("
|
|
77
|
-
.option("--dry-run", "
|
|
76
|
+
.description("配置 MCP — 自动配置 AI Agent 的 MCP 连接")
|
|
77
|
+
.option("--dry-run", "仅展示配置内容,不实际写入")
|
|
78
78
|
.action((opts) => setupMcpCommand(opts));
|
|
79
79
|
// ===== Interactive mode (no command) =====
|
|
80
80
|
program.action(async () => {
|
|
81
81
|
ui.banner();
|
|
82
82
|
const isLoggedIn = !!getApiKey();
|
|
83
83
|
if (!isLoggedIn) {
|
|
84
|
-
ui.info("
|
|
84
|
+
ui.info("欢迎使用 Dreamlogic!让我们先完成登录。");
|
|
85
85
|
console.log();
|
|
86
86
|
await loginCommand({});
|
|
87
87
|
if (!getApiKey())
|
|
@@ -91,15 +91,15 @@ program.action(async () => {
|
|
|
91
91
|
// Interactive menu with @clack/prompts
|
|
92
92
|
while (true) {
|
|
93
93
|
const action = await clack.select({
|
|
94
|
-
message: "
|
|
94
|
+
message: "请选择操作:",
|
|
95
95
|
options: [
|
|
96
|
-
{ value: "install", label: "
|
|
97
|
-
{ value: "update", label: "
|
|
98
|
-
{ value: "list", label: "
|
|
99
|
-
{ value: "setup-mcp", label: "
|
|
100
|
-
{ value: "status", label: "
|
|
101
|
-
{ value: "catalog", label: "
|
|
102
|
-
{ value: "exit", label: "
|
|
96
|
+
{ value: "install", label: "📦 安装技能", hint: "从服务器下载新技能" },
|
|
97
|
+
{ value: "update", label: "⬆️ 检查更新", hint: "升级已安装的技能" },
|
|
98
|
+
{ value: "list", label: "📋 已安装列表", hint: "查看本地安装情况" },
|
|
99
|
+
{ value: "setup-mcp", label: "🔗 配置 MCP", hint: "自动配置 Claude/Cursor/Cline 等" },
|
|
100
|
+
{ value: "status", label: "📊 状态概览", hint: "完整系统状态" },
|
|
101
|
+
{ value: "catalog", label: "🔍 浏览技能目录", hint: "查看全部可用技能" },
|
|
102
|
+
{ value: "exit", label: "👋 退出" },
|
|
103
103
|
],
|
|
104
104
|
});
|
|
105
105
|
if (clack.isCancel(action)) {
|
|
@@ -136,8 +136,8 @@ program.exitOverride();
|
|
|
136
136
|
async function main() {
|
|
137
137
|
// BUG-2: Warn if TLS verification is disabled
|
|
138
138
|
if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0") {
|
|
139
|
-
ui.warning("NODE_TLS_REJECT_UNAUTHORIZED=0
|
|
140
|
-
ui.warning("
|
|
139
|
+
ui.warning("检测到 NODE_TLS_REJECT_UNAUTHORIZED=0 — TLS 证书验证已禁用");
|
|
140
|
+
ui.warning("存在安全风险,建议移除:set NODE_TLS_REJECT_UNAUTHORIZED=");
|
|
141
141
|
}
|
|
142
142
|
try {
|
|
143
143
|
await program.parseAsync(process.argv);
|
|
@@ -156,7 +156,7 @@ async function main() {
|
|
|
156
156
|
clack.cancel("Cancelled.");
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
|
-
ui.err(
|
|
159
|
+
ui.err(`意外错误: ${err.message}`);
|
|
160
160
|
process.exitCode = 1;
|
|
161
161
|
}
|
|
162
162
|
}
|
package/dist/lib/agents.js
CHANGED
|
@@ -39,13 +39,20 @@ const AGENT_CONFIGS = [
|
|
|
39
39
|
{ name: "Aider", skillsDir: ".aider/skills", group: "additional", detect: ".aider" },
|
|
40
40
|
{ name: "Plandex", skillsDir: ".plandex/skills", group: "additional", detect: ".plandex" },
|
|
41
41
|
];
|
|
42
|
+
// R4-FIX: Shared safe skill ID pattern
|
|
43
|
+
const SAFE_SKILL_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
42
44
|
/**
|
|
43
45
|
* Resolve the skills directory for an agent (handles env overrides + home expansion)
|
|
44
46
|
*/
|
|
45
47
|
function resolveAgentDir(agent) {
|
|
46
48
|
const home = homedir();
|
|
47
49
|
if (agent.envOverride && process.env[agent.envOverride]) {
|
|
48
|
-
|
|
50
|
+
const dir = join(process.env[agent.envOverride], "skills");
|
|
51
|
+
// R4-FIX: Prevent env override pointing outside home dir
|
|
52
|
+
if (!resolve(dir).startsWith(resolve(home))) {
|
|
53
|
+
throw new Error(`Agent dir must be under home directory: ${dir}`);
|
|
54
|
+
}
|
|
55
|
+
return dir;
|
|
49
56
|
}
|
|
50
57
|
return join(home, agent.skillsDir);
|
|
51
58
|
}
|
|
@@ -141,6 +148,9 @@ export function registerSkillWithAgents(skillId, skillPath, agents) {
|
|
|
141
148
|
* Unregister a skill from all agents (remove symlinks only)
|
|
142
149
|
*/
|
|
143
150
|
export function unregisterSkillFromAgents(skillId) {
|
|
151
|
+
// R4-FIX: Validate skillId to prevent path traversal (same as registerSkillWithAgents)
|
|
152
|
+
if (!SAFE_SKILL_ID.test(skillId))
|
|
153
|
+
return 0;
|
|
144
154
|
const home = homedir();
|
|
145
155
|
let removed = 0;
|
|
146
156
|
// Check all known agent dirs
|
|
@@ -163,6 +173,9 @@ export function unregisterSkillFromAgents(skillId) {
|
|
|
163
173
|
* List which agents have a specific skill registered
|
|
164
174
|
*/
|
|
165
175
|
export function getSkillAgentStatus(skillId) {
|
|
176
|
+
// R4-FIX: Validate skillId to prevent path traversal info leak
|
|
177
|
+
if (!SAFE_SKILL_ID.test(skillId))
|
|
178
|
+
return [];
|
|
166
179
|
const results = [];
|
|
167
180
|
const seenDirs = new Set();
|
|
168
181
|
for (const agent of AGENT_CONFIGS) {
|
package/dist/lib/config.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Key stored with file permissions 600 (owner-only)
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
6
|
+
import { join, resolve } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
import { CONFIG_DIR_NAME, DEFAULT_SERVER, DEFAULT_INSTALL_DIR_NAME, } from "../types.js";
|
|
9
9
|
/** CFG-01 FIX: Recursively strip prototype pollution keys from parsed JSON */
|
|
@@ -21,8 +21,8 @@ function sanitize(obj) {
|
|
|
21
21
|
}
|
|
22
22
|
return obj;
|
|
23
23
|
}
|
|
24
|
-
// R1-06: Key format validation applied everywhere
|
|
25
|
-
const KEY_RE = /^sk-(admin|user)-[a-f0-9]{16,}$/;
|
|
24
|
+
// R1-06: Key format validation applied everywhere — R4-FIX: add upper bound
|
|
25
|
+
const KEY_RE = /^sk-(admin|user)-[a-f0-9]{16,128}$/;
|
|
26
26
|
function getConfigDir() {
|
|
27
27
|
return join(homedir(), CONFIG_DIR_NAME);
|
|
28
28
|
}
|
|
@@ -37,9 +37,8 @@ export function getDefaultInstallDir() {
|
|
|
37
37
|
}
|
|
38
38
|
export function ensureConfigDir() {
|
|
39
39
|
const dir = getConfigDir();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
40
|
+
// R4-FIX: mkdirSync with recursive handles existence; no TOCTOU
|
|
41
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
43
42
|
}
|
|
44
43
|
export function loadConfig() {
|
|
45
44
|
const path = getConfigPath();
|
|
@@ -86,10 +85,14 @@ export function getServer() {
|
|
|
86
85
|
return url;
|
|
87
86
|
}
|
|
88
87
|
export function getInstallDir() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
const dir = process.env.DREAMLOGIC_INSTALL_DIR || loadConfig()?.install_dir || getDefaultInstallDir();
|
|
89
|
+
const resolved = resolve(dir);
|
|
90
|
+
// R4-FIX: Block system-critical paths
|
|
91
|
+
const BLOCKED = ["/usr", "/bin", "/sbin", "/etc", "/var", "/System", "/Library", "/Windows", "/Program Files"];
|
|
92
|
+
if (BLOCKED.some(p => resolved.toLowerCase().startsWith(p.toLowerCase()))) {
|
|
93
|
+
throw new Error(`安装目录不能是系统关键路径: ${resolved}`);
|
|
94
|
+
}
|
|
95
|
+
return resolved;
|
|
93
96
|
}
|
|
94
97
|
export function loadInstalled() {
|
|
95
98
|
const path = getInstalledPath();
|
package/dist/lib/installer.js
CHANGED
|
@@ -39,11 +39,11 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
|
|
|
39
39
|
const { createHash } = await import("crypto");
|
|
40
40
|
buffer = readFileSync(opts.fromFile);
|
|
41
41
|
actualSha256 = createHash("sha256").update(buffer).digest("hex");
|
|
42
|
-
ui.info(
|
|
42
|
+
ui.info(`从本地文件加载: ${opts.fromFile}`);
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
45
45
|
// Download with progress
|
|
46
|
-
const spinner = ui.spinner(
|
|
46
|
+
const spinner = ui.spinner(`正在下载 ${skillId} ${expectedVersion}...`);
|
|
47
47
|
spinner.start();
|
|
48
48
|
try {
|
|
49
49
|
const result = await client.downloadPackage(packageFile, (dl, total) => {
|
|
@@ -53,25 +53,26 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
|
|
|
53
53
|
});
|
|
54
54
|
buffer = result.buffer;
|
|
55
55
|
actualSha256 = result.sha256;
|
|
56
|
-
spinner.succeed(`
|
|
56
|
+
spinner.succeed(` 已下载 ${ui.fileSize(buffer.length)}`);
|
|
57
57
|
}
|
|
58
58
|
catch (err) {
|
|
59
|
-
spinner.fail(`
|
|
59
|
+
spinner.fail(` 下载失败`);
|
|
60
60
|
throw err;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
// SHA256 verification
|
|
63
|
+
// SHA256 verification — R4-FIX: no-hash is a security event, not just a warning
|
|
64
64
|
if (expectedSha256) {
|
|
65
65
|
if (actualSha256 !== expectedSha256) {
|
|
66
|
-
ui.err("SHA256
|
|
67
|
-
ui.line(`
|
|
68
|
-
ui.line(`
|
|
66
|
+
ui.err("SHA256 校验失败!文件可能已损坏或被篡改。");
|
|
67
|
+
ui.line(` 期望值: ${expectedSha256}`);
|
|
68
|
+
ui.line(` 实际值: ${actualSha256}`);
|
|
69
69
|
throw new Error("SHA256 verification failed");
|
|
70
70
|
}
|
|
71
|
-
ui.ok("SHA256
|
|
71
|
+
ui.ok("SHA256 校验通过");
|
|
72
72
|
}
|
|
73
73
|
else {
|
|
74
|
-
ui.warning("
|
|
74
|
+
ui.warning("⚠️ 服务器未提供 SHA256 校验值 — 无法验证包完整性");
|
|
75
|
+
ui.warning(" 这可能意味着包被篡改或服务器配置错误");
|
|
75
76
|
}
|
|
76
77
|
// Extract to staging directory (D-05: never extract directly to target)
|
|
77
78
|
// R1-10 + R2-01/R2-02: Signal cleanup with proper listener management
|
|
@@ -86,15 +87,15 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
|
|
|
86
87
|
process.on("SIGTERM", cleanupAndExit);
|
|
87
88
|
// R2-01: try/finally ensures listeners are always removed
|
|
88
89
|
try {
|
|
89
|
-
const extractSpinner = ui.spinner("
|
|
90
|
+
const extractSpinner = ui.spinner("正在解压...");
|
|
90
91
|
extractSpinner.start();
|
|
91
92
|
try {
|
|
92
93
|
mkdirSync(stagingDir, { recursive: true });
|
|
93
94
|
await extractZip(buffer, stagingDir);
|
|
94
|
-
extractSpinner.succeed("
|
|
95
|
+
extractSpinner.succeed(" 解压完成");
|
|
95
96
|
}
|
|
96
97
|
catch (err) {
|
|
97
|
-
extractSpinner.fail("
|
|
98
|
+
extractSpinner.fail(" 解压失败");
|
|
98
99
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
99
100
|
throw err;
|
|
100
101
|
}
|
|
@@ -102,7 +103,7 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
|
|
|
102
103
|
try {
|
|
103
104
|
if (existsSync(skillDir)) {
|
|
104
105
|
renameSync(skillDir, backupDir);
|
|
105
|
-
ui.info(
|
|
106
|
+
ui.info(`已备份上一版本`);
|
|
106
107
|
}
|
|
107
108
|
const extractedRoot = findExtractedRoot(stagingDir);
|
|
108
109
|
renameSync(extractedRoot, skillDir);
|
|
@@ -147,7 +148,7 @@ export async function installSkill(client, skillId, packageFile, expectedSha256,
|
|
|
147
148
|
}
|
|
148
149
|
catch { /* best-effort cleanup */ }
|
|
149
150
|
if (existsSync(backupDir)) {
|
|
150
|
-
ui.info(
|
|
151
|
+
ui.info(`可回滚: dreamlogic rollback ${skillId}`);
|
|
151
152
|
}
|
|
152
153
|
return { path: skillDir, version: expectedVersion };
|
|
153
154
|
}
|
|
@@ -195,6 +196,10 @@ export function rollbackSkill(skillId) {
|
|
|
195
196
|
*/
|
|
196
197
|
function findExtractedRoot(stagingDir) {
|
|
197
198
|
const entries = readdirSync(stagingDir);
|
|
199
|
+
// R4-FIX: Reject empty packages
|
|
200
|
+
if (entries.length === 0) {
|
|
201
|
+
throw new Error("解压后的包为空,不包含任何文件");
|
|
202
|
+
}
|
|
198
203
|
// If only one directory entry, use that as root
|
|
199
204
|
if (entries.length === 1) {
|
|
200
205
|
const single = join(stagingDir, entries[0]);
|
package/dist/lib/ui.d.ts
CHANGED
|
@@ -27,9 +27,9 @@ export declare const ui: {
|
|
|
27
27
|
dimText(msg: string): void;
|
|
28
28
|
/** Create an ora spinner with brand styling */
|
|
29
29
|
spinner(text: string): Ora;
|
|
30
|
-
/** Format file size */
|
|
30
|
+
/** Format file size — R4-FIX: handle NaN/negative */
|
|
31
31
|
fileSize(bytes: number): string;
|
|
32
|
-
/** Format a skill for display (rich) */
|
|
32
|
+
/** Format a skill for display (rich) — R4-FIX: sanitize server-provided strings */
|
|
33
33
|
skillLine(s: {
|
|
34
34
|
id: string;
|
|
35
35
|
name: string;
|
package/dist/lib/ui.js
CHANGED
|
@@ -21,6 +21,13 @@ const muted = chalk.hex("#6B7280");
|
|
|
21
21
|
const hasTruecolor = chalk.level >= 3;
|
|
22
22
|
// ===== Gradient helper =====
|
|
23
23
|
const brandGradient = gradientString(BRAND_GRADIENT);
|
|
24
|
+
// R4-FIX: Strip ANSI escape sequences and control chars from untrusted strings
|
|
25
|
+
function stripAnsi(str) {
|
|
26
|
+
return str
|
|
27
|
+
.replace(/[\x00-\x1F\x7F-\x9F]/g, "") // control chars
|
|
28
|
+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "") // CSI sequences
|
|
29
|
+
.replace(/\x1b\][^\x07]*\x07/g, ""); // OSC sequences
|
|
30
|
+
}
|
|
24
31
|
export const ui = {
|
|
25
32
|
brand,
|
|
26
33
|
accent,
|
|
@@ -92,21 +99,23 @@ export const ui = {
|
|
|
92
99
|
spinner: "dots",
|
|
93
100
|
});
|
|
94
101
|
},
|
|
95
|
-
/** Format file size */
|
|
102
|
+
/** Format file size — R4-FIX: handle NaN/negative */
|
|
96
103
|
fileSize(bytes) {
|
|
104
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
105
|
+
return "unknown";
|
|
97
106
|
if (bytes < 1024)
|
|
98
107
|
return `${bytes} B`;
|
|
99
108
|
if (bytes < 1024 * 1024)
|
|
100
109
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
101
110
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
102
111
|
},
|
|
103
|
-
/** Format a skill for display (rich) */
|
|
112
|
+
/** Format a skill for display (rich) — R4-FIX: sanitize server-provided strings */
|
|
104
113
|
skillLine(s) {
|
|
105
|
-
const name = chalk.bold.white(s.name);
|
|
106
|
-
const ver = s.version ? muted(` v${s.version}`) : "";
|
|
114
|
+
const name = chalk.bold.white(stripAnsi(s.name));
|
|
115
|
+
const ver = s.version ? muted(` v${stripAnsi(s.version)}`) : "";
|
|
107
116
|
const size = s.size ? muted(` (${ui.fileSize(s.size)})`) : "";
|
|
108
|
-
const desc = muted(s.description);
|
|
109
|
-
const tag = s.tag ? ` ${s.tag}` : "";
|
|
117
|
+
const desc = muted(stripAnsi(s.description));
|
|
118
|
+
const tag = s.tag ? ` ${stripAnsi(s.tag)}` : "";
|
|
110
119
|
return `${name}${ver}${size}\n ${desc}${tag}`;
|
|
111
120
|
},
|
|
112
121
|
/** Print a key-value table with alignment */
|
package/dist/types.d.ts
CHANGED
|
@@ -33,6 +33,6 @@ export interface InstalledRegistry {
|
|
|
33
33
|
export declare const DEFAULT_SERVER = "https://skill.dreamlogic-claw.com";
|
|
34
34
|
export declare const DEFAULT_INSTALL_DIR_NAME = "dreamlogic-skills";
|
|
35
35
|
export declare const CONFIG_DIR_NAME = ".dreamlogic";
|
|
36
|
-
export declare const CLI_VERSION = "2.0.
|
|
36
|
+
export declare const CLI_VERSION = "2.0.4";
|
|
37
37
|
export declare const CLI_NAME = "Dreamlogic CLI";
|
|
38
38
|
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
|
+
export const CLI_VERSION = "2.0.4";
|
|
6
6
|
export const CLI_NAME = "Dreamlogic CLI";
|
|
7
7
|
export const CLI_AUTHOR = "Dreamlogic-ai by MAJORNINE";
|