chainlesschain 0.43.2 → 0.43.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/package.json +4 -2
- package/src/commands/init.js +2252 -1
- package/src/commands/skill.js +166 -0
- package/src/lib/skill-loader.js +4 -0
- package/src/lib/skill-packs/generator.js +630 -0
- package/src/lib/skill-packs/schema.js +462 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Command Skill Pack Generator
|
|
3
|
+
*
|
|
4
|
+
* Auto-generates SKILL.md + handler.js for each CLI domain pack.
|
|
5
|
+
* Output goes to <userData>/skills/ (managed layer, globally available).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { generateCliPacks } from './generator.js';
|
|
9
|
+
* const result = await generateCliPacks({ force: false, dryRun: false });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import crypto from "crypto";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import {
|
|
17
|
+
CLI_PACK_DOMAINS,
|
|
18
|
+
EXECUTION_MODE_DESCRIPTIONS,
|
|
19
|
+
AGENT_MODE_COMMANDS,
|
|
20
|
+
PACK_SCHEMA_VERSION,
|
|
21
|
+
} from "./schema.js";
|
|
22
|
+
import { getElectronUserDataDir } from "../paths.js";
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
// ── Hash utilities ─────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute a version hash for a domain pack.
|
|
30
|
+
* Hash changes when: schema version, CLI version, or command list changes.
|
|
31
|
+
*/
|
|
32
|
+
export function computePackHash(domainKey, domainDef, cliVersion) {
|
|
33
|
+
const commands = Object.keys(domainDef.commands).sort().join(",");
|
|
34
|
+
const raw = `${PACK_SCHEMA_VERSION}|${cliVersion}|${domainKey}|${commands}`;
|
|
35
|
+
return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read the current CLI package version
|
|
40
|
+
*/
|
|
41
|
+
function getCliVersion() {
|
|
42
|
+
try {
|
|
43
|
+
const pkgPath = path.resolve(__dirname, "../../../package.json");
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
45
|
+
return pkg.version || "0.0.0";
|
|
46
|
+
} catch {
|
|
47
|
+
return "0.0.0";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read the version hash stored in an existing SKILL.md
|
|
53
|
+
*/
|
|
54
|
+
function readExistingHash(skillMdPath) {
|
|
55
|
+
try {
|
|
56
|
+
if (!fs.existsSync(skillMdPath)) return null;
|
|
57
|
+
const content = fs.readFileSync(skillMdPath, "utf-8");
|
|
58
|
+
const match = content.match(/cli-version-hash:\s*["']?([a-f0-9]+)["']?/);
|
|
59
|
+
return match ? match[1] : null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── SKILL.md generator ─────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate SKILL.md content for a domain pack
|
|
69
|
+
*/
|
|
70
|
+
export function generateSkillMd(domainKey, domainDef, hash) {
|
|
71
|
+
const commandEntries = Object.entries(domainDef.commands);
|
|
72
|
+
const allTags = domainDef.tags.join(", ");
|
|
73
|
+
|
|
74
|
+
// Build commands documentation section
|
|
75
|
+
const commandDocs = commandEntries
|
|
76
|
+
.map(([cmd, info]) => {
|
|
77
|
+
const subCmds =
|
|
78
|
+
info.subcommands && info.subcommands.length > 0
|
|
79
|
+
? `\n 子命令: \`${info.subcommands.join("`、`")}\``
|
|
80
|
+
: "";
|
|
81
|
+
const example = info.example
|
|
82
|
+
? `\n 示例: \`chainlesschain ${info.example}\``
|
|
83
|
+
: "";
|
|
84
|
+
const agentNote =
|
|
85
|
+
info.isAgentMode || AGENT_MODE_COMMANDS.has(cmd)
|
|
86
|
+
? "\n ⚠️ 此指令需要在终端中直接运行"
|
|
87
|
+
: "";
|
|
88
|
+
return `### \`${cmd}\`\n${info.description}${subCmds}${example}${agentNote}`;
|
|
89
|
+
})
|
|
90
|
+
.join("\n\n");
|
|
91
|
+
|
|
92
|
+
// Build input format examples
|
|
93
|
+
const inputExamples = commandEntries
|
|
94
|
+
.filter((_, i) => i < 3)
|
|
95
|
+
.map(([, info]) => info.example || "")
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((ex) => `- "${ex}"`)
|
|
98
|
+
.join("\n");
|
|
99
|
+
|
|
100
|
+
const modeDesc = EXECUTION_MODE_DESCRIPTIONS[domainDef.executionMode] || "";
|
|
101
|
+
const agentWarning =
|
|
102
|
+
domainDef.executionMode === "agent"
|
|
103
|
+
? "\n\n> **注意**: 此技能包中的指令需要交互式终端,handler不会直接执行指令,而是返回完整的使用说明供Agent决策。"
|
|
104
|
+
: "";
|
|
105
|
+
|
|
106
|
+
return `---
|
|
107
|
+
name: ${domainKey}
|
|
108
|
+
display-name: ${domainDef.displayName}
|
|
109
|
+
description: ${domainDef.description}
|
|
110
|
+
version: 1.0.0
|
|
111
|
+
category: ${domainDef.category}
|
|
112
|
+
execution-mode: ${domainDef.executionMode}
|
|
113
|
+
cli-domain: ${domainKey.replace("cli-", "").replace("-pack", "")}
|
|
114
|
+
cli-version-hash: "${hash}"
|
|
115
|
+
tags: [${allTags}]
|
|
116
|
+
user-invocable: true
|
|
117
|
+
handler: handler.js
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
# ${domainDef.displayName}
|
|
121
|
+
|
|
122
|
+
${domainDef.description}${agentWarning}
|
|
123
|
+
|
|
124
|
+
## 执行模式
|
|
125
|
+
|
|
126
|
+
**${domainDef.executionMode}** — ${modeDesc}
|
|
127
|
+
|
|
128
|
+
## 调用方式
|
|
129
|
+
|
|
130
|
+
\`\`\`
|
|
131
|
+
chainlesschain skill run ${domainKey} "<command> [subcommand] [args] [--options]"
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
**输入格式示例**:
|
|
135
|
+
${inputExamples}
|
|
136
|
+
|
|
137
|
+
## 包含指令
|
|
138
|
+
|
|
139
|
+
${commandDocs}
|
|
140
|
+
|
|
141
|
+
## 全局选项
|
|
142
|
+
|
|
143
|
+
所有指令支持以下全局选项:
|
|
144
|
+
- \`--verbose\` 详细日志输出
|
|
145
|
+
- \`--quiet\` 静默模式
|
|
146
|
+
- \`--json\` JSON格式输出(部分指令支持)
|
|
147
|
+
|
|
148
|
+
## 提供商配置(适用于AI相关指令)
|
|
149
|
+
|
|
150
|
+
- \`--provider <name>\` 指定LLM提供商 (ollama/openai/anthropic/deepseek等)
|
|
151
|
+
- \`--model <name>\` 指定模型名称
|
|
152
|
+
- \`--api-key <key>\` API密钥
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Handler generator ──────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate handler.js content for a direct-execution pack
|
|
160
|
+
*/
|
|
161
|
+
function generateDirectHandler(domainKey, domainDef) {
|
|
162
|
+
const commandList = Object.keys(domainDef.commands).join('", "');
|
|
163
|
+
const commandGuide = Object.entries(domainDef.commands)
|
|
164
|
+
.map(
|
|
165
|
+
([cmd, info]) =>
|
|
166
|
+
` ${cmd}: '${info.example ? `chainlesschain ${info.example}` : `chainlesschain ${cmd}`}'`,
|
|
167
|
+
)
|
|
168
|
+
.join(",\n");
|
|
169
|
+
|
|
170
|
+
return `/**
|
|
171
|
+
* ${domainDef.displayName} — 直接执行处理器
|
|
172
|
+
*
|
|
173
|
+
* 执行模式: ${domainDef.executionMode}
|
|
174
|
+
* 包含指令: ${Object.keys(domainDef.commands).join(", ")}
|
|
175
|
+
*
|
|
176
|
+
* 自动生成 — 请勿手动修改,运行 \`chainlesschain skill sync-cli\` 重新生成
|
|
177
|
+
*/
|
|
178
|
+
|
|
179
|
+
const { spawnSync } = require("child_process");
|
|
180
|
+
|
|
181
|
+
/** 解析输入字符串为指令+参数数组 */
|
|
182
|
+
function parseInput(input) {
|
|
183
|
+
if (!input || !input.trim()) return { args: [], raw: "" };
|
|
184
|
+
// Shell-style tokenizer (handles quoted strings)
|
|
185
|
+
const args = [];
|
|
186
|
+
let current = "";
|
|
187
|
+
let inSingle = false;
|
|
188
|
+
let inDouble = false;
|
|
189
|
+
for (const ch of input.trim()) {
|
|
190
|
+
if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
|
|
191
|
+
if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
|
|
192
|
+
if (ch === " " && !inSingle && !inDouble) {
|
|
193
|
+
if (current) { args.push(current); current = ""; }
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
current += ch;
|
|
197
|
+
}
|
|
198
|
+
if (current) args.push(current);
|
|
199
|
+
return { args, raw: input.trim() };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 检测是否支持 --json 输出的指令 */
|
|
203
|
+
const JSON_SUPPORTED_COMMANDS = new Set([
|
|
204
|
+
"note", "search", "did", "wallet", "org", "p2p",
|
|
205
|
+
"session", "tokens", "llm", "skill", "mcp", "plugin",
|
|
206
|
+
"bi", "lowcode", "compliance", "siem", "hook", "workflow", "a2a", "hmemory"
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
const VALID_COMMANDS = new Set(["${commandList}"]);
|
|
210
|
+
|
|
211
|
+
const handler = {
|
|
212
|
+
async init(_skill) {
|
|
213
|
+
// No initialization needed for direct execution
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async execute(task, context, _skill) {
|
|
217
|
+
const input = task.input || task.params?.input || "";
|
|
218
|
+
const { args, raw } = parseInput(input);
|
|
219
|
+
|
|
220
|
+
if (!args.length) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
error: "请提供要执行的指令。示例:\\n" + JSON.stringify({
|
|
224
|
+
${commandGuide}
|
|
225
|
+
}, null, 2),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const command = args[0];
|
|
230
|
+
|
|
231
|
+
// Validate command belongs to this pack
|
|
232
|
+
if (!VALID_COMMANDS.has(command)) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: \`指令 "\${command}" 不在此技能包中。可用指令: \${[...VALID_COMMANDS].join(", ")}\`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Build CLI args — add --json for structured output when supported
|
|
240
|
+
const cliArgs = [...args];
|
|
241
|
+
const useJson = JSON_SUPPORTED_COMMANDS.has(command) && !cliArgs.includes("--json");
|
|
242
|
+
if (useJson) cliArgs.push("--json");
|
|
243
|
+
|
|
244
|
+
// Execute via child process
|
|
245
|
+
const result = spawnSync("chainlesschain", cliArgs, {
|
|
246
|
+
encoding: "utf-8",
|
|
247
|
+
shell: true,
|
|
248
|
+
cwd: context.projectRoot || process.cwd(),
|
|
249
|
+
timeout: 30000,
|
|
250
|
+
env: { ...process.env },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (result.error) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
error: \`执行失败: \${result.error.message}\`,
|
|
257
|
+
command: \`chainlesschain \${raw}\`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const stdout = (result.stdout || "").trim();
|
|
262
|
+
const stderr = (result.stderr || "").trim();
|
|
263
|
+
const exitCode = result.status ?? -1;
|
|
264
|
+
|
|
265
|
+
if (exitCode !== 0) {
|
|
266
|
+
return {
|
|
267
|
+
success: false,
|
|
268
|
+
error: stderr || \`指令退出码: \${exitCode}\`,
|
|
269
|
+
stdout,
|
|
270
|
+
command: \`chainlesschain \${raw}\`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Try to parse JSON output for structured result
|
|
275
|
+
let parsed = null;
|
|
276
|
+
if (useJson && stdout) {
|
|
277
|
+
try { parsed = JSON.parse(stdout); } catch { /* plain text output */ }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
success: true,
|
|
282
|
+
message: \`chainlesschain \${cliArgs.slice(0, 2).join(" ")} 执行成功\`,
|
|
283
|
+
result: parsed || stdout || "(无输出)",
|
|
284
|
+
command: \`chainlesschain \${raw}\`,
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
module.exports = handler;
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generate handler.js content for agent-mode pack
|
|
295
|
+
*/
|
|
296
|
+
function generateAgentHandler(domainKey, domainDef) {
|
|
297
|
+
const usageGuide = Object.entries(domainDef.commands)
|
|
298
|
+
.map(([cmd, info]) => {
|
|
299
|
+
const ex = info.example
|
|
300
|
+
? `chainlesschain ${info.example}`
|
|
301
|
+
: `chainlesschain ${cmd}`;
|
|
302
|
+
return ` { command: '${cmd}', description: '${info.description}', example: '${ex}' }`;
|
|
303
|
+
})
|
|
304
|
+
.join(",\n");
|
|
305
|
+
|
|
306
|
+
return `/**
|
|
307
|
+
* ${domainDef.displayName} — Agent模式处理器
|
|
308
|
+
*
|
|
309
|
+
* 执行模式: ${domainDef.executionMode}
|
|
310
|
+
* ⚠️ 此技能包中的指令需要交互式终端,不能通过子进程直接调用。
|
|
311
|
+
* handler返回使用说明,由上层Agent决策如何通知用户执行。
|
|
312
|
+
*
|
|
313
|
+
* 自动生成 — 请勿手动修改,运行 \`chainlesschain skill sync-cli\` 重新生成
|
|
314
|
+
*/
|
|
315
|
+
|
|
316
|
+
const COMMANDS = [
|
|
317
|
+
${usageGuide}
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const handler = {
|
|
321
|
+
async init(_skill) {},
|
|
322
|
+
|
|
323
|
+
async execute(task, _context, _skill) {
|
|
324
|
+
const input = (task.input || task.params?.input || "").trim();
|
|
325
|
+
|
|
326
|
+
// Match requested command
|
|
327
|
+
const requestedCmd = input.split(/\\s+/)[0] || "";
|
|
328
|
+
const matched = requestedCmd
|
|
329
|
+
? COMMANDS.find(c => c.command === requestedCmd)
|
|
330
|
+
: null;
|
|
331
|
+
|
|
332
|
+
if (matched) {
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
executionMode: "agent",
|
|
336
|
+
message: \`\${matched.command} 需要在终端中交互式运行\`,
|
|
337
|
+
result: {
|
|
338
|
+
description: matched.description,
|
|
339
|
+
howToRun: \`在终端中执行: \${matched.example}\`,
|
|
340
|
+
note: "此指令需要交互式REPL,请直接在终端运行上方命令",
|
|
341
|
+
options: {
|
|
342
|
+
"--provider <name>": "LLM提供商 (ollama/openai/anthropic/...)",
|
|
343
|
+
"--model <name>": "模型名称",
|
|
344
|
+
"--session <id>": "恢复上次会话",
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Return full command guide
|
|
351
|
+
return {
|
|
352
|
+
success: true,
|
|
353
|
+
executionMode: "agent",
|
|
354
|
+
message: "以下指令需要在终端中直接运行",
|
|
355
|
+
result: {
|
|
356
|
+
availableCommands: COMMANDS.map(c => ({
|
|
357
|
+
command: c.command,
|
|
358
|
+
description: c.description,
|
|
359
|
+
example: c.example,
|
|
360
|
+
})),
|
|
361
|
+
note: "这些指令需要交互式终端 (REPL),请直接在终端中运行对应命令",
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
module.exports = handler;
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Generate handler.js content for hybrid-mode pack
|
|
373
|
+
*/
|
|
374
|
+
function generateHybridHandler(domainKey, domainDef) {
|
|
375
|
+
const agentCmds = Object.entries(domainDef.commands)
|
|
376
|
+
.filter(([cmd, info]) => info.isAgentMode || AGENT_MODE_COMMANDS.has(cmd))
|
|
377
|
+
.map(([cmd]) => `"${cmd}"`)
|
|
378
|
+
.join(", ");
|
|
379
|
+
|
|
380
|
+
const commandList = Object.keys(domainDef.commands).join('", "');
|
|
381
|
+
const commandGuide = Object.entries(domainDef.commands)
|
|
382
|
+
.filter(([cmd, info]) => !info.isAgentMode && !AGENT_MODE_COMMANDS.has(cmd))
|
|
383
|
+
.map(
|
|
384
|
+
([cmd, info]) =>
|
|
385
|
+
` ${cmd}: '${info.example ? `chainlesschain ${info.example}` : `chainlesschain ${cmd}`}'`,
|
|
386
|
+
)
|
|
387
|
+
.join(",\n");
|
|
388
|
+
|
|
389
|
+
return `/**
|
|
390
|
+
* ${domainDef.displayName} — 混合执行处理器
|
|
391
|
+
*
|
|
392
|
+
* 执行模式: ${domainDef.executionMode}
|
|
393
|
+
* Agent模式指令 (需终端): [${agentCmds || "无"}]
|
|
394
|
+
* 其余指令通过子进程直接执行
|
|
395
|
+
*
|
|
396
|
+
* 自动生成 — 请勿手动修改,运行 \`chainlesschain skill sync-cli\` 重新生成
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
const { spawnSync } = require("child_process");
|
|
400
|
+
|
|
401
|
+
const AGENT_ONLY_COMMANDS = new Set([${agentCmds}]);
|
|
402
|
+
const VALID_COMMANDS = new Set(["${commandList}"]);
|
|
403
|
+
|
|
404
|
+
function parseInput(input) {
|
|
405
|
+
if (!input || !input.trim()) return { args: [], raw: "" };
|
|
406
|
+
const args = [];
|
|
407
|
+
let current = "";
|
|
408
|
+
let inSingle = false;
|
|
409
|
+
let inDouble = false;
|
|
410
|
+
for (const ch of input.trim()) {
|
|
411
|
+
if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
|
|
412
|
+
if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
|
|
413
|
+
if (ch === " " && !inSingle && !inDouble) {
|
|
414
|
+
if (current) { args.push(current); current = ""; }
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
current += ch;
|
|
418
|
+
}
|
|
419
|
+
if (current) args.push(current);
|
|
420
|
+
return { args, raw: input.trim() };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const handler = {
|
|
424
|
+
async init(_skill) {},
|
|
425
|
+
|
|
426
|
+
async execute(task, context, _skill) {
|
|
427
|
+
const input = task.input || task.params?.input || "";
|
|
428
|
+
const { args, raw } = parseInput(input);
|
|
429
|
+
|
|
430
|
+
if (!args.length) {
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
error: "请提供要执行的指令。可用指令: " + [...VALID_COMMANDS].join(", "),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const command = args[0];
|
|
438
|
+
|
|
439
|
+
if (!VALID_COMMANDS.has(command)) {
|
|
440
|
+
return {
|
|
441
|
+
success: false,
|
|
442
|
+
error: \`指令 "\${command}" 不在此技能包中。可用指令: \${[...VALID_COMMANDS].join(", ")}\`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Agent-only commands: return usage guide
|
|
447
|
+
if (AGENT_ONLY_COMMANDS.has(command)) {
|
|
448
|
+
return {
|
|
449
|
+
success: true,
|
|
450
|
+
executionMode: "agent",
|
|
451
|
+
message: \`\${command} 需要在终端中交互式运行\`,
|
|
452
|
+
result: {
|
|
453
|
+
howToRun: \`chainlesschain \${raw}\`,
|
|
454
|
+
note: "此指令需要交互式终端,请直接在终端中运行",
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Direct execution for other commands
|
|
460
|
+
const cliArgs = [...args, "--quiet"];
|
|
461
|
+
const result = spawnSync("chainlesschain", cliArgs, {
|
|
462
|
+
encoding: "utf-8",
|
|
463
|
+
shell: true,
|
|
464
|
+
cwd: context.projectRoot || process.cwd(),
|
|
465
|
+
timeout: 30000,
|
|
466
|
+
env: { ...process.env },
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (result.error) {
|
|
470
|
+
return { success: false, error: \`执行失败: \${result.error.message}\` };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const exitCode = result.status ?? -1;
|
|
474
|
+
if (exitCode !== 0) {
|
|
475
|
+
return {
|
|
476
|
+
success: false,
|
|
477
|
+
error: (result.stderr || "").trim() || \`退出码: \${exitCode}\`,
|
|
478
|
+
stdout: (result.stdout || "").trim(),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let parsed = null;
|
|
483
|
+
const stdout = (result.stdout || "").trim();
|
|
484
|
+
try { parsed = JSON.parse(stdout); } catch { /* plain text */ }
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
message: \`chainlesschain \${command} 执行成功\`,
|
|
489
|
+
result: parsed || stdout || "(无输出)",
|
|
490
|
+
};
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
module.exports = handler;
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ── Main generator ─────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Generate all CLI pack skills
|
|
502
|
+
*
|
|
503
|
+
* @param {object} options
|
|
504
|
+
* @param {boolean} [options.force=false] - Force regenerate even if unchanged
|
|
505
|
+
* @param {boolean} [options.dryRun=false] - Preview changes without writing
|
|
506
|
+
* @param {string} [options.outputDir] - Override output directory (default: managed layer)
|
|
507
|
+
* @returns {Promise<GeneratorResult>}
|
|
508
|
+
*/
|
|
509
|
+
export async function generateCliPacks(options = {}) {
|
|
510
|
+
const { force = false, dryRun = false } = options;
|
|
511
|
+
|
|
512
|
+
const cliVersion = getCliVersion();
|
|
513
|
+
const outputDir =
|
|
514
|
+
options.outputDir || path.join(getElectronUserDataDir(), "skills");
|
|
515
|
+
|
|
516
|
+
const results = {
|
|
517
|
+
generated: [],
|
|
518
|
+
skipped: [],
|
|
519
|
+
errors: [],
|
|
520
|
+
outputDir,
|
|
521
|
+
cliVersion,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
for (const [domainKey, domainDef] of Object.entries(CLI_PACK_DOMAINS)) {
|
|
525
|
+
const packDir = path.join(outputDir, domainKey);
|
|
526
|
+
const skillMdPath = path.join(packDir, "SKILL.md");
|
|
527
|
+
const handlerPath = path.join(packDir, "handler.js");
|
|
528
|
+
|
|
529
|
+
const newHash = computePackHash(domainKey, domainDef, cliVersion);
|
|
530
|
+
const existingHash = readExistingHash(skillMdPath);
|
|
531
|
+
|
|
532
|
+
// Skip if unchanged (unless forced)
|
|
533
|
+
if (!force && existingHash === newHash) {
|
|
534
|
+
results.skipped.push({ domain: domainKey, reason: "unchanged" });
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const changeReason =
|
|
539
|
+
existingHash === null ? "new" : force ? "forced" : "hash-changed";
|
|
540
|
+
|
|
541
|
+
if (dryRun) {
|
|
542
|
+
results.generated.push({ domain: domainKey, dryRun: true, changeReason });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
// Create directory
|
|
548
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
549
|
+
|
|
550
|
+
// Generate SKILL.md
|
|
551
|
+
const skillMd = generateSkillMd(domainKey, domainDef, newHash);
|
|
552
|
+
fs.writeFileSync(skillMdPath, skillMd, "utf-8");
|
|
553
|
+
|
|
554
|
+
// Generate handler.js based on execution mode
|
|
555
|
+
let handlerJs;
|
|
556
|
+
if (domainDef.executionMode === "agent") {
|
|
557
|
+
handlerJs = generateAgentHandler(domainKey, domainDef);
|
|
558
|
+
} else if (domainDef.executionMode === "hybrid") {
|
|
559
|
+
handlerJs = generateHybridHandler(domainKey, domainDef);
|
|
560
|
+
} else {
|
|
561
|
+
// direct or llm-query — both use spawnSync
|
|
562
|
+
handlerJs = generateDirectHandler(domainKey, domainDef);
|
|
563
|
+
}
|
|
564
|
+
fs.writeFileSync(handlerPath, handlerJs, "utf-8");
|
|
565
|
+
|
|
566
|
+
results.generated.push({
|
|
567
|
+
domain: domainKey,
|
|
568
|
+
displayName: domainDef.displayName,
|
|
569
|
+
executionMode: domainDef.executionMode,
|
|
570
|
+
commandCount: Object.keys(domainDef.commands).length,
|
|
571
|
+
packDir,
|
|
572
|
+
changeReason,
|
|
573
|
+
hash: newHash,
|
|
574
|
+
});
|
|
575
|
+
} catch (err) {
|
|
576
|
+
results.errors.push({ domain: domainKey, error: err.message });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return results;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Check which packs need updating (without generating)
|
|
585
|
+
* @param {string} [outputDir]
|
|
586
|
+
* @returns {object[]} Array of packs that need updating
|
|
587
|
+
*/
|
|
588
|
+
export function checkForUpdates(outputDir) {
|
|
589
|
+
const dir = outputDir || path.join(getElectronUserDataDir(), "skills");
|
|
590
|
+
const cliVersion = getCliVersion();
|
|
591
|
+
const updates = [];
|
|
592
|
+
|
|
593
|
+
for (const [domainKey, domainDef] of Object.entries(CLI_PACK_DOMAINS)) {
|
|
594
|
+
const skillMdPath = path.join(dir, domainKey, "SKILL.md");
|
|
595
|
+
const existingHash = readExistingHash(skillMdPath);
|
|
596
|
+
const newHash = computePackHash(domainKey, domainDef, cliVersion);
|
|
597
|
+
|
|
598
|
+
if (existingHash !== newHash) {
|
|
599
|
+
updates.push({
|
|
600
|
+
domain: domainKey,
|
|
601
|
+
displayName: domainDef.displayName,
|
|
602
|
+
exists: existingHash !== null,
|
|
603
|
+
changeReason: existingHash === null ? "new" : "hash-changed",
|
|
604
|
+
existingHash,
|
|
605
|
+
newHash,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return updates;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Remove all generated CLI packs
|
|
615
|
+
* @param {string} [outputDir]
|
|
616
|
+
*/
|
|
617
|
+
export function removeCliPacks(outputDir) {
|
|
618
|
+
const dir = outputDir || path.join(getElectronUserDataDir(), "skills");
|
|
619
|
+
const removed = [];
|
|
620
|
+
|
|
621
|
+
for (const domainKey of Object.keys(CLI_PACK_DOMAINS)) {
|
|
622
|
+
const packDir = path.join(dir, domainKey);
|
|
623
|
+
if (fs.existsSync(packDir)) {
|
|
624
|
+
fs.rmSync(packDir, { recursive: true, force: true });
|
|
625
|
+
removed.push(domainKey);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return removed;
|
|
630
|
+
}
|