@seandong/seno 0.1.0

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.
Files changed (104) hide show
  1. package/README.md +70 -0
  2. package/dist/agent/conversation.d.ts +39 -0
  3. package/dist/agent/conversation.js +60 -0
  4. package/dist/agent/conversation.js.map +1 -0
  5. package/dist/agent/loop.d.ts +41 -0
  6. package/dist/agent/loop.js +203 -0
  7. package/dist/agent/loop.js.map +1 -0
  8. package/dist/agent/session.d.ts +63 -0
  9. package/dist/agent/session.js +135 -0
  10. package/dist/agent/session.js.map +1 -0
  11. package/dist/cli/commands.d.ts +52 -0
  12. package/dist/cli/commands.js +667 -0
  13. package/dist/cli/commands.js.map +1 -0
  14. package/dist/cli/logger.d.ts +38 -0
  15. package/dist/cli/logger.js +79 -0
  16. package/dist/cli/logger.js.map +1 -0
  17. package/dist/cli/output.d.ts +75 -0
  18. package/dist/cli/output.js +305 -0
  19. package/dist/cli/output.js.map +1 -0
  20. package/dist/cli/prompt.d.ts +30 -0
  21. package/dist/cli/prompt.js +196 -0
  22. package/dist/cli/prompt.js.map +1 -0
  23. package/dist/cli/repl.d.ts +27 -0
  24. package/dist/cli/repl.js +485 -0
  25. package/dist/cli/repl.js.map +1 -0
  26. package/dist/commands/init.d.ts +4 -0
  27. package/dist/commands/init.js +170 -0
  28. package/dist/commands/init.js.map +1 -0
  29. package/dist/commands/model.d.ts +10 -0
  30. package/dist/commands/model.js +270 -0
  31. package/dist/commands/model.js.map +1 -0
  32. package/dist/config/manager.d.ts +67 -0
  33. package/dist/config/manager.js +194 -0
  34. package/dist/config/manager.js.map +1 -0
  35. package/dist/config/types.d.ts +98 -0
  36. package/dist/config/types.js +2 -0
  37. package/dist/config/types.js.map +1 -0
  38. package/dist/errors.d.ts +37 -0
  39. package/dist/errors.js +54 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +185 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/llm/anthropic.d.ts +27 -0
  45. package/dist/llm/anthropic.js +189 -0
  46. package/dist/llm/anthropic.js.map +1 -0
  47. package/dist/llm/factory.d.ts +47 -0
  48. package/dist/llm/factory.js +163 -0
  49. package/dist/llm/factory.js.map +1 -0
  50. package/dist/llm/openai-codex.d.ts +45 -0
  51. package/dist/llm/openai-codex.js +398 -0
  52. package/dist/llm/openai-codex.js.map +1 -0
  53. package/dist/llm/openai.d.ts +16 -0
  54. package/dist/llm/openai.js +288 -0
  55. package/dist/llm/openai.js.map +1 -0
  56. package/dist/llm/provider.d.ts +19 -0
  57. package/dist/llm/provider.js +2 -0
  58. package/dist/llm/provider.js.map +1 -0
  59. package/dist/llm/types.d.ts +102 -0
  60. package/dist/llm/types.js +2 -0
  61. package/dist/llm/types.js.map +1 -0
  62. package/dist/mcp/bridge.d.ts +30 -0
  63. package/dist/mcp/bridge.js +73 -0
  64. package/dist/mcp/bridge.js.map +1 -0
  65. package/dist/mcp/config.d.ts +6 -0
  66. package/dist/mcp/config.js +26 -0
  67. package/dist/mcp/config.js.map +1 -0
  68. package/dist/mcp/manager.d.ts +54 -0
  69. package/dist/mcp/manager.js +171 -0
  70. package/dist/mcp/manager.js.map +1 -0
  71. package/dist/prompts/system.d.ts +14 -0
  72. package/dist/prompts/system.js +194 -0
  73. package/dist/prompts/system.js.map +1 -0
  74. package/dist/skills/loader.d.ts +7 -0
  75. package/dist/skills/loader.js +81 -0
  76. package/dist/skills/loader.js.map +1 -0
  77. package/dist/skills/registry.d.ts +48 -0
  78. package/dist/skills/registry.js +104 -0
  79. package/dist/skills/registry.js.map +1 -0
  80. package/dist/skills/sync.d.ts +34 -0
  81. package/dist/skills/sync.js +179 -0
  82. package/dist/skills/sync.js.map +1 -0
  83. package/dist/skills/types.d.ts +29 -0
  84. package/dist/skills/types.js +2 -0
  85. package/dist/skills/types.js.map +1 -0
  86. package/dist/tools/ask.d.ts +16 -0
  87. package/dist/tools/ask.js +57 -0
  88. package/dist/tools/ask.js.map +1 -0
  89. package/dist/tools/registry.d.ts +54 -0
  90. package/dist/tools/registry.js +114 -0
  91. package/dist/tools/registry.js.map +1 -0
  92. package/dist/tools/shell.d.ts +10 -0
  93. package/dist/tools/shell.js +131 -0
  94. package/dist/tools/shell.js.map +1 -0
  95. package/dist/tools/ssh.d.ts +40 -0
  96. package/dist/tools/ssh.js +302 -0
  97. package/dist/tools/ssh.js.map +1 -0
  98. package/dist/tools/types.d.ts +20 -0
  99. package/dist/tools/types.js +2 -0
  100. package/dist/tools/types.js.map +1 -0
  101. package/dist/utils/retry.d.ts +20 -0
  102. package/dist/utils/retry.js +33 -0
  103. package/dist/utils/retry.js.map +1 -0
  104. package/package.json +51 -0
@@ -0,0 +1,667 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { execSync } from 'child_process';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { loadServers, saveServers } from '../config/manager.js';
8
+ import { syncSkills } from '../skills/sync.js';
9
+ import { CancelError, promptQuestion, promptChoice } from './prompt.js';
10
+ import { modelInfo, modelSet, modelAuth } from '../commands/model.js';
11
+ export const COMMAND_TREE = [
12
+ {
13
+ name: 'skills',
14
+ description: '管理 Skills',
15
+ subCommands: [
16
+ { name: 'list', description: '列出所有 Skills' },
17
+ { name: 'sync', description: '手动同步远程 Skills' },
18
+ ],
19
+ },
20
+ {
21
+ name: 'server',
22
+ description: '管理 SSH 服务器',
23
+ subCommands: [
24
+ { name: 'list', description: '列出所有服务器' },
25
+ { name: 'add', description: '添加新服务器' },
26
+ { name: 'remove', description: '删除指定服务器' },
27
+ ],
28
+ },
29
+ {
30
+ name: 'plugin',
31
+ description: 'ONES 插件开发助手',
32
+ subCommands: [
33
+ { name: 'init', description: '初始化新插件项目' },
34
+ ],
35
+ },
36
+ {
37
+ name: 'model',
38
+ description: '管理 LLM 模型配置',
39
+ subCommands: [
40
+ { name: 'set', description: '切换 Provider 和模型' },
41
+ { name: 'auth', description: '配置 Provider 授权' },
42
+ ],
43
+ },
44
+ { name: 'clear', description: '清空对话历史' },
45
+ { name: 'exit', description: '退出 SENO CLI' },
46
+ { name: 'help', description: '显示帮助信息' },
47
+ ];
48
+ /**
49
+ * 根据当前输入计算候选列表
50
+ * @param input 当前输入(含 /),保留尾部空格用于触发子命令候选
51
+ */
52
+ export function getCommandCandidates(input) {
53
+ if (!input.startsWith('/'))
54
+ return [];
55
+ const body = input.slice(1); // 去掉 /
56
+ const hasSpace = body.includes(' ');
57
+ if (!hasSpace) {
58
+ // 主命令阶段:"/", "/he", "/server"(无空格)
59
+ const prefix = body.toLowerCase();
60
+ return COMMAND_TREE.filter((cmd) => cmd.name.startsWith(prefix)).map((cmd) => ({
61
+ text: `/${cmd.name}`,
62
+ description: cmd.description,
63
+ }));
64
+ }
65
+ // 子命令阶段:"/server ", "/server li" 等(含空格)
66
+ const spaceIndex = body.indexOf(' ');
67
+ const mainName = body.slice(0, spaceIndex).toLowerCase();
68
+ const subInput = body.slice(spaceIndex + 1).trimStart().toLowerCase();
69
+ const mainCmd = COMMAND_TREE.find((c) => c.name === mainName);
70
+ if (!mainCmd?.subCommands)
71
+ return [];
72
+ return mainCmd.subCommands
73
+ .filter((sub) => sub.name.startsWith(subInput))
74
+ .map((sub) => ({
75
+ text: `/${mainName} ${sub.name}`,
76
+ description: sub.description,
77
+ }));
78
+ }
79
+ // ─── Command Execution ───────────────────────────────────────
80
+ /**
81
+ * 判断输入是否为斜杠命令
82
+ */
83
+ export function isSlashCommand(input) {
84
+ return input.startsWith('/');
85
+ }
86
+ /**
87
+ * 处理斜杠命令
88
+ * @returns false 表示应退出 REPL 循环
89
+ */
90
+ export async function handleSlashCommand(input, context) {
91
+ const parts = input.trim().split(/\s+/);
92
+ const command = parts[0].toLowerCase();
93
+ switch (command) {
94
+ case '/help':
95
+ printHelp();
96
+ return true;
97
+ case '/model':
98
+ return handleModelCommand(parts.slice(1), context);
99
+ case '/skills':
100
+ return handleSkillsCommand(parts.slice(1), context);
101
+ case '/server':
102
+ return handleServerCommand(parts.slice(1), context);
103
+ case '/plugin':
104
+ return handlePluginCommand(parts.slice(1), context);
105
+ case '/clear':
106
+ context.agentLoop.clearHistory();
107
+ console.log(chalk.green(' ✓ 对话历史已清空'));
108
+ return true;
109
+ case '/exit':
110
+ console.log(chalk.gray(' 再见 👋'));
111
+ return false;
112
+ default:
113
+ console.log(chalk.yellow(` 未知命令: ${command}`) +
114
+ chalk.gray(',输入 /help 查看可用命令'));
115
+ return true;
116
+ }
117
+ }
118
+ // ─── /help ────────────────────────────────────────────────────
119
+ function printHelp() {
120
+ console.log();
121
+ console.log(chalk.bold(' 可用命令:'));
122
+ console.log();
123
+ for (const cmd of COMMAND_TREE) {
124
+ if (cmd.subCommands) {
125
+ for (const sub of cmd.subCommands) {
126
+ const full = `/${cmd.name} ${sub.name}`;
127
+ console.log(` ${chalk.cyan(full.padEnd(22))}${sub.description}`);
128
+ }
129
+ }
130
+ else {
131
+ console.log(` ${chalk.cyan(`/${cmd.name}`.padEnd(22))}${cmd.description}`);
132
+ }
133
+ }
134
+ console.log();
135
+ console.log(chalk.gray(' 直接输入文本与 AI Agent 对话'));
136
+ console.log();
137
+ }
138
+ // ─── /skills ──────────────────────────────────────────────────
139
+ function printSkills(skills) {
140
+ if (skills.length === 0) {
141
+ console.log(chalk.yellow('\n 没有可用的 Skills\n'));
142
+ return;
143
+ }
144
+ console.log();
145
+ console.log(chalk.bold(' Available Skills:'));
146
+ console.log();
147
+ // 计算列宽
148
+ const nameWidth = Math.max(6, ...skills.map((s) => s.name.length));
149
+ const versionWidth = Math.max(7, ...skills.map((s) => (s.version || '-').length));
150
+ const domainWidth = Math.max(8, ...skills.map((s) => s.domain.length));
151
+ // 表头
152
+ console.log(` ${chalk.gray('Name'.padEnd(nameWidth + 2))}` +
153
+ `${chalk.gray('Version'.padEnd(versionWidth + 2))}` +
154
+ `${chalk.gray('Domain'.padEnd(domainWidth + 2))}` +
155
+ `${chalk.gray('Description')}`);
156
+ console.log(chalk.gray(` ${'─'.repeat(nameWidth + versionWidth + domainWidth + 46)}`));
157
+ // 行
158
+ for (const skill of skills) {
159
+ const desc = skill.description.length > 40
160
+ ? skill.description.slice(0, 40) + '...'
161
+ : skill.description;
162
+ const ver = skill.version || '-';
163
+ console.log(` ${skill.name.padEnd(nameWidth + 2)}` +
164
+ `${chalk.green(ver.padEnd(versionWidth + 2))}` +
165
+ `${chalk.cyan(skill.domain.padEnd(domainWidth + 2))}` +
166
+ `${chalk.gray(desc)}`);
167
+ }
168
+ console.log();
169
+ }
170
+ // ─── /skills ─────────────────────────────────────────────────
171
+ async function handleSkillsCommand(args, context) {
172
+ const subCommand = args[0]?.toLowerCase();
173
+ switch (subCommand) {
174
+ case 'sync':
175
+ return skillsSync(context);
176
+ case 'list':
177
+ case undefined:
178
+ printSkills(context.skills);
179
+ return true;
180
+ default:
181
+ console.log(chalk.yellow(` 未知子命令: ${subCommand}`) +
182
+ chalk.gray(',可用: list, sync'));
183
+ return true;
184
+ }
185
+ }
186
+ async function skillsSync(context) {
187
+ const spinner = ora({ text: '同步 Skills...', indent: 2, discardStdin: false });
188
+ spinner.start();
189
+ try {
190
+ const result = await syncSkills(context.config);
191
+ if (result.success) {
192
+ spinner.succeed(`Skills 已同步 (${result.skillCount} 个技能)`);
193
+ // 重新加载 skills
194
+ context.skills = await context.reloadSkills();
195
+ }
196
+ else {
197
+ spinner.fail(`Skills 同步失败: ${result.error}`);
198
+ }
199
+ }
200
+ catch (error) {
201
+ spinner.fail(`Skills 同步失败: ${error instanceof Error ? error.message : String(error)}`);
202
+ }
203
+ return true;
204
+ }
205
+ // ─── /model ──────────────────────────────────────────────────
206
+ async function handleModelCommand(args, context) {
207
+ const subCommand = args[0]?.toLowerCase();
208
+ try {
209
+ switch (subCommand) {
210
+ case 'set':
211
+ await modelSet(context.rl, context.config, context.switchLLMProvider);
212
+ return true;
213
+ case 'auth':
214
+ await modelAuth(context.rl, context.config, context.switchLLMProvider);
215
+ return true;
216
+ case undefined:
217
+ await modelInfo(context.config);
218
+ return true;
219
+ default:
220
+ console.log(chalk.yellow(` 未知子命令: ${subCommand}`) +
221
+ chalk.gray(',可用: set, auth'));
222
+ return true;
223
+ }
224
+ }
225
+ catch (error) {
226
+ if (error instanceof CancelError) {
227
+ console.log();
228
+ return true;
229
+ }
230
+ throw error;
231
+ }
232
+ }
233
+ // ─── /server ──────────────────────────────────────────────────
234
+ async function handleServerCommand(args, context) {
235
+ const subCommand = args[0]?.toLowerCase();
236
+ switch (subCommand) {
237
+ case 'list':
238
+ case undefined:
239
+ await serverList();
240
+ return true;
241
+ case 'add':
242
+ return serverAdd(context.rl);
243
+ case 'remove': {
244
+ const name = args[1];
245
+ if (!name) {
246
+ console.log(chalk.yellow(' 用法: /server remove <name>'));
247
+ return true;
248
+ }
249
+ await serverRemove(name);
250
+ return true;
251
+ }
252
+ default:
253
+ console.log(chalk.yellow(` 未知子命令: ${subCommand}`) +
254
+ chalk.gray(',可用: list, add, remove'));
255
+ return true;
256
+ }
257
+ }
258
+ async function serverList() {
259
+ const { servers } = await loadServers();
260
+ if (servers.length === 0) {
261
+ console.log(chalk.yellow('\n 没有已配置的服务器,使用 /server add 添加\n'));
262
+ return;
263
+ }
264
+ console.log();
265
+ console.log(chalk.bold(' SSH 服务器:'));
266
+ console.log();
267
+ const nameWidth = Math.max(6, ...servers.map((s) => s.name.length));
268
+ const hostWidth = Math.max(6, ...servers.map((s) => s.host.length));
269
+ const userWidth = Math.max(6, ...servers.map((s) => s.user.length));
270
+ const authWidth = 10;
271
+ console.log(` ${chalk.gray('Name'.padEnd(nameWidth + 2))}` +
272
+ `${chalk.gray('Host'.padEnd(hostWidth + 2))}` +
273
+ `${chalk.gray('User'.padEnd(userWidth + 2))}` +
274
+ `${chalk.gray('Port')} ` +
275
+ `${chalk.gray('Auth'.padEnd(authWidth))}` +
276
+ `${chalk.gray('Description')}`);
277
+ console.log(chalk.gray(` ${'─'.repeat(nameWidth + hostWidth + userWidth + authWidth + 30)}`));
278
+ for (const s of servers) {
279
+ const auth = s.auth_type === 'password' ? 'password' : 'ssh-key';
280
+ console.log(` ${s.name.padEnd(nameWidth + 2)}` +
281
+ `${s.host.padEnd(hostWidth + 2)}` +
282
+ `${s.user.padEnd(userWidth + 2)}` +
283
+ `${String(s.port).padEnd(6)}` +
284
+ `${auth.padEnd(authWidth)}` +
285
+ `${chalk.gray(s.description || '')}`);
286
+ }
287
+ console.log();
288
+ }
289
+ async function serverAdd(rl) {
290
+ try {
291
+ console.log();
292
+ console.log(chalk.gray(' 添加服务器 [按 ESC 键取消]'));
293
+ console.log();
294
+ const name = await promptQuestion(rl, chalk.cyan(' Server name: '));
295
+ if (!name) {
296
+ console.log(chalk.yellow(' 服务器名称不能为空'));
297
+ return true;
298
+ }
299
+ const host = await promptQuestion(rl, chalk.cyan(' Host: '));
300
+ if (!host) {
301
+ console.log(chalk.yellow(' 主机地址不能为空'));
302
+ return true;
303
+ }
304
+ const userInput = await promptQuestion(rl, chalk.cyan(' User [root]: '));
305
+ const user = userInput || 'root';
306
+ const portInput = await promptQuestion(rl, chalk.cyan(' Port [22]: '));
307
+ const port = portInput ? parseInt(portInput, 10) : 22;
308
+ if (isNaN(port)) {
309
+ console.log(chalk.yellow(' 端口号无效'));
310
+ return true;
311
+ }
312
+ // 认证方式选择
313
+ console.log();
314
+ console.log(chalk.cyan(' Auth method:'));
315
+ const authIdx = await promptChoice(rl, '', [
316
+ 'SSH Key',
317
+ 'Password',
318
+ ], 0);
319
+ const authType = authIdx === 1 ? 'password' : 'ssh-key';
320
+ let identity_file;
321
+ let password;
322
+ if (authType === 'ssh-key') {
323
+ const identityInput = await promptQuestion(rl, chalk.cyan(' Identity file [~/.ssh/id_rsa]: '));
324
+ identity_file = identityInput || '~/.ssh/id_rsa';
325
+ }
326
+ else {
327
+ password = await promptQuestion(rl, chalk.cyan(' Password: '));
328
+ if (!password) {
329
+ console.log(chalk.yellow(' 密码不能为空'));
330
+ return true;
331
+ }
332
+ }
333
+ const description = await promptQuestion(rl, chalk.cyan(' Description: '));
334
+ // 保存
335
+ const config = await loadServers();
336
+ // 检查重名
337
+ if (config.servers.some((s) => s.name === name)) {
338
+ console.log(chalk.yellow(`\n 服务器 "${name}" 已存在,请使用其他名称\n`));
339
+ return true;
340
+ }
341
+ config.servers.push({
342
+ name,
343
+ host,
344
+ user,
345
+ port,
346
+ auth_type: authType,
347
+ identity_file,
348
+ password,
349
+ description,
350
+ });
351
+ await saveServers(config);
352
+ console.log(chalk.green(`\n ✓ Server "${name}" added successfully\n`));
353
+ return true;
354
+ }
355
+ catch (error) {
356
+ if (error instanceof CancelError) {
357
+ // ESC 取消,静默返回
358
+ console.log();
359
+ return true;
360
+ }
361
+ throw error;
362
+ }
363
+ }
364
+ async function serverRemove(name) {
365
+ const config = await loadServers();
366
+ const index = config.servers.findIndex((s) => s.name === name);
367
+ if (index === -1) {
368
+ console.log(chalk.yellow(`\n 未找到服务器: ${name}\n`));
369
+ return;
370
+ }
371
+ config.servers.splice(index, 1);
372
+ await saveServers(config);
373
+ console.log(chalk.green(`\n ✓ Server "${name}" removed\n`));
374
+ }
375
+ // ─── /plugin ─────────────────────────────────────────────────
376
+ /**
377
+ * 安全执行 shell 命令,返回 stdout 或 null(失败时)
378
+ */
379
+ function runCommand(cmd, timeout = 15000) {
380
+ try {
381
+ return execSync(cmd, {
382
+ encoding: 'utf-8',
383
+ timeout,
384
+ stdio: ['pipe', 'pipe', 'pipe'],
385
+ }).trim();
386
+ }
387
+ catch {
388
+ return null;
389
+ }
390
+ }
391
+ /** 获取 nvm 目录路径 */
392
+ function getNvmDir() {
393
+ return process.env.NVM_DIR || path.join(process.env.HOME || '~', '.nvm');
394
+ }
395
+ /** 检查 nvm 是否已安装 */
396
+ function isNvmInstalled() {
397
+ const nvmScript = path.join(getNvmDir(), 'nvm.sh');
398
+ try {
399
+ fs.accessSync(nvmScript);
400
+ return true;
401
+ }
402
+ catch {
403
+ return false;
404
+ }
405
+ }
406
+ /** 通过 nvm 执行命令(先 source nvm.sh),通过 stdin 传入脚本避免引号问题 */
407
+ function runWithNvm(cmd) {
408
+ const nvmDir = getNvmDir();
409
+ const script = `source "${nvmDir}/nvm.sh"\n${cmd}`;
410
+ try {
411
+ return execSync('bash', {
412
+ input: script,
413
+ encoding: 'utf-8',
414
+ timeout: 300000,
415
+ stdio: ['pipe', 'pipe', 'pipe'],
416
+ }).trim();
417
+ }
418
+ catch {
419
+ return null;
420
+ }
421
+ }
422
+ /** 检查 nvm 中是否已安装 Node.js 18,返回版本号或 null */
423
+ function getNode18Version() {
424
+ const result = runWithNvm('nvm ls 18');
425
+ if (!result)
426
+ return null;
427
+ const match = result.match(/v(18\.\d+\.\d+)/);
428
+ return match ? match[1] : null;
429
+ }
430
+ /** 确保 nvm 已安装,返回是否成功 */
431
+ async function ensureNvm() {
432
+ if (isNvmInstalled())
433
+ return true;
434
+ const spinner = ora({ text: '安装 nvm...', indent: 2, discardStdin: false });
435
+ spinner.start();
436
+ try {
437
+ execSync('curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 120000 });
438
+ if (!isNvmInstalled()) {
439
+ spinner.fail('nvm 安装失败');
440
+ return false;
441
+ }
442
+ spinner.succeed('nvm 安装成功');
443
+ return true;
444
+ }
445
+ catch {
446
+ spinner.fail('nvm 安装失败');
447
+ return false;
448
+ }
449
+ }
450
+ /** 确保 nvm 中存在 Node.js 18(仅安装,不切换当前版本) */
451
+ async function ensureNode18() {
452
+ // 1. 确保 nvm 已安装
453
+ if (!(await ensureNvm()))
454
+ return false;
455
+ // 2. 检查 nvm 中是否已有 v18
456
+ const existing = getNode18Version();
457
+ if (existing) {
458
+ console.log(chalk.green(' ✓ ') + `${'Node.js 18'.padEnd(12)}${chalk.gray(`v${existing}`)}`);
459
+ return true;
460
+ }
461
+ // 3. nvm 中没有 v18,安装
462
+ const spinner = ora({ text: '通过 nvm 安装 Node.js 18...', indent: 2, discardStdin: false });
463
+ spinner.start();
464
+ runWithNvm('nvm install 18');
465
+ const installed = getNode18Version();
466
+ if (installed) {
467
+ spinner.succeed(`Node.js 18 安装成功 ${chalk.gray(`v${installed}`)}`);
468
+ return true;
469
+ }
470
+ spinner.fail('Node.js 18 安装失败');
471
+ return false;
472
+ }
473
+ /** 自动安装 ONES CLI */
474
+ async function installOnesCli() {
475
+ const spinner = ora({ text: '安装 ONES CLI...', indent: 2, discardStdin: false });
476
+ spinner.start();
477
+ try {
478
+ execSync('npm install -g @ones/cli --registry=https://npm.partner.ones.cn/registry/', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 120000 });
479
+ const version = runCommand('ones --version');
480
+ if (version) {
481
+ spinner.succeed(`ONES CLI 安装成功 ${chalk.gray(version)}`);
482
+ return true;
483
+ }
484
+ }
485
+ catch { /* fall through */ }
486
+ spinner.fail('ONES CLI 安装失败');
487
+ console.log(chalk.gray(' 手动安装: npm install -g @ones/cli --registry=https://npm.partner.ones.cn/registry/'));
488
+ return false;
489
+ }
490
+ /** 自动安装 npx(通过 npm) */
491
+ async function installNpx() {
492
+ const spinner = ora({ text: '安装 npx...', indent: 2, discardStdin: false });
493
+ spinner.start();
494
+ try {
495
+ execSync('npm install -g npx', {
496
+ encoding: 'utf-8',
497
+ stdio: ['pipe', 'pipe', 'pipe'],
498
+ timeout: 120000,
499
+ });
500
+ const version = runCommand('npx --version');
501
+ if (version) {
502
+ spinner.succeed(`npx 安装成功 ${chalk.gray(version)}`);
503
+ return true;
504
+ }
505
+ }
506
+ catch { /* fall through */ }
507
+ spinner.fail('npx 安装失败');
508
+ console.log(chalk.gray(' 手动安装: npm install -g npx'));
509
+ return false;
510
+ }
511
+ async function handlePluginCommand(args, context) {
512
+ const subCommand = args[0]?.toLowerCase();
513
+ switch (subCommand) {
514
+ case 'init':
515
+ return pluginInit(context.rl);
516
+ case undefined:
517
+ console.log(chalk.yellow(' 用法: /plugin init') +
518
+ chalk.gray(' 初始化新插件项目'));
519
+ return true;
520
+ default:
521
+ console.log(chalk.yellow(` 未知子命令: ${subCommand}`) +
522
+ chalk.gray(',可用: init'));
523
+ return true;
524
+ }
525
+ }
526
+ async function pluginInit(rl) {
527
+ try {
528
+ console.log();
529
+ console.log(chalk.bold(' ONES 插件项目初始化'));
530
+ console.log(chalk.gray(' [按 ESC 键取消]'));
531
+ console.log();
532
+ // ── 环境预检 ──────────────────────────────────────────
533
+ console.log(chalk.bold(' 环境预检:'));
534
+ console.log();
535
+ // Node.js 18 通过 nvm 单独检查(不依赖当前激活版本)
536
+ const node18Ok = await ensureNode18();
537
+ const checks = [
538
+ { label: 'npm', cmd: 'npm --version' },
539
+ {
540
+ label: 'ONES CLI',
541
+ cmd: 'ones --version',
542
+ autoFix: installOnesCli,
543
+ },
544
+ {
545
+ label: 'npx',
546
+ cmd: 'npx --version',
547
+ autoFix: installNpx,
548
+ },
549
+ ];
550
+ let allRequiredPassed = node18Ok;
551
+ for (const check of checks) {
552
+ let version = runCommand(check.cmd);
553
+ if (version) {
554
+ console.log(chalk.green(' ✓ ') +
555
+ `${check.label.padEnd(12)}${chalk.gray(version)}`);
556
+ continue;
557
+ }
558
+ // 未通过 - 尝试自动修复
559
+ if (check.autoFix) {
560
+ const fixed = await check.autoFix();
561
+ if (fixed) {
562
+ version = runCommand(check.cmd);
563
+ if (version)
564
+ continue;
565
+ }
566
+ }
567
+ // 无法自动修复或修复失败
568
+ allRequiredPassed = false;
569
+ console.log(chalk.red(' ✗ ') + `${check.label} 未安装`);
570
+ }
571
+ console.log();
572
+ if (!allRequiredPassed) {
573
+ console.log(chalk.yellow(' 请先安装缺失的依赖,然后重新执行 /plugin init\n'));
574
+ return true;
575
+ }
576
+ console.log(chalk.green(' 环境预检通过'));
577
+ console.log();
578
+ // ── 收集插件信息 ────────────────────────────────────────
579
+ const pluginName = await promptQuestion(rl, chalk.cyan(' 插件名称: '));
580
+ if (!pluginName) {
581
+ console.log(chalk.yellow(' 插件名称不能为空'));
582
+ return true;
583
+ }
584
+ // 校验名称格式(仅允许字母、数字、连字符、下划线)
585
+ if (!/^[a-zA-Z0-9_-]+$/.test(pluginName)) {
586
+ console.log(chalk.yellow(' 插件名称只能包含字母、数字、连字符(-)和下划线(_)'));
587
+ return true;
588
+ }
589
+ const pluginDir = await promptQuestion(rl, chalk.cyan(' 项目目录: ') + chalk.gray('(留空为当前目录) '));
590
+ // ── 创建项目 ──────────────────────────────────────────
591
+ // ones create -d <name> → 在当前目录创建项目
592
+ // ones create -d <name> -s <dir> → 在 <dir>/ 创建项目
593
+ const projectPath = path.resolve(pluginDir || '.');
594
+ // 确保 -s 目标目录存在(ones create 要求该路径已存在)
595
+ if (pluginDir) {
596
+ try {
597
+ fs.mkdirSync(path.resolve(pluginDir), { recursive: true });
598
+ }
599
+ catch {
600
+ console.log(chalk.red(`\n 目录创建失败: ${pluginDir}\n`));
601
+ return true;
602
+ }
603
+ }
604
+ console.log();
605
+ console.log(chalk.gray(` 正在创建插件项目: ${pluginName}`) +
606
+ (pluginDir ? chalk.gray(` → ${pluginDir}/`) : '') +
607
+ chalk.gray(' ...'));
608
+ console.log();
609
+ let createCmd = `ones create -d ${pluginName}`;
610
+ if (pluginDir) {
611
+ createCmd += ` -s ${pluginDir}`;
612
+ }
613
+ const appId = crypto.randomBytes(4).toString('hex');
614
+ try {
615
+ execSync(createCmd, {
616
+ input: appId + '\n',
617
+ stdio: ['pipe', 'inherit', 'inherit'],
618
+ timeout: 300000,
619
+ });
620
+ }
621
+ catch (error) {
622
+ const err = error;
623
+ if (err.status) {
624
+ console.log(chalk.red(`\n 创建失败 (exit code: ${err.status})\n`));
625
+ }
626
+ else {
627
+ console.log(chalk.red(`\n 创建失败: ${err.message ?? error}\n`));
628
+ }
629
+ return true;
630
+ }
631
+ // ones create 可能 exit 0 但实际失败,检查项目目录是否存在
632
+ if (!fs.existsSync(projectPath)) {
633
+ console.log(chalk.red(`\n 创建失败: 项目目录不存在 ${projectPath}\n`));
634
+ return true;
635
+ }
636
+ console.log(chalk.green(` ✓ 插件项目 "${pluginName}" 创建成功`));
637
+ // ── 写入 .nvmrc ─────────────────────────────────────────
638
+ try {
639
+ fs.writeFileSync(path.join(projectPath, '.nvmrc'), '18\n', 'utf-8');
640
+ console.log(chalk.green(' ✓ ') + '.nvmrc 已写入');
641
+ }
642
+ catch {
643
+ console.log(chalk.yellow(' ⚠ .nvmrc 写入失败'));
644
+ }
645
+ console.log();
646
+ // ── 后续指引 ──────────────────────────────────────────
647
+ console.log(chalk.bold(' 本地调试步骤:'));
648
+ console.log();
649
+ console.log(chalk.cyan(' 1. ') + `cd ${projectPath}`);
650
+ console.log(chalk.cyan(' 2. ') + 'npx op login');
651
+ console.log(chalk.cyan(' 3. ') + 'npx op pickteam local');
652
+ console.log(chalk.cyan(' 4. ') + 'npx op invoke run');
653
+ console.log();
654
+ console.log(chalk.bold(' 开发文档: ') +
655
+ chalk.underline('https://developer.ones.cn/zh-CN/docs/guide/getting-started/development'));
656
+ console.log();
657
+ return true;
658
+ }
659
+ catch (error) {
660
+ if (error instanceof CancelError) {
661
+ console.log();
662
+ return true;
663
+ }
664
+ throw error;
665
+ }
666
+ }
667
+ //# sourceMappingURL=commands.js.map