helloagents 1.0.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 (36) hide show
  1. package/Claude/Skills/CN/CLAUDE.md +998 -0
  2. package/Claude/Skills/CN/skills/helloagents/analyze/SKILL.md +187 -0
  3. package/Claude/Skills/CN/skills/helloagents/design/SKILL.md +261 -0
  4. package/Claude/Skills/CN/skills/helloagents/develop/SKILL.md +352 -0
  5. package/Claude/Skills/CN/skills/helloagents/kb/SKILL.md +249 -0
  6. package/Claude/Skills/CN/skills/helloagents/templates/SKILL.md +451 -0
  7. package/Claude/Skills/EN/CLAUDE.md +998 -0
  8. package/Claude/Skills/EN/skills/helloagents/analyze/SKILL.md +187 -0
  9. package/Claude/Skills/EN/skills/helloagents/design/SKILL.md +261 -0
  10. package/Claude/Skills/EN/skills/helloagents/develop/SKILL.md +352 -0
  11. package/Claude/Skills/EN/skills/helloagents/kb/SKILL.md +249 -0
  12. package/Claude/Skills/EN/skills/helloagents/templates/SKILL.md +451 -0
  13. package/Codex/Skills/CN/AGENTS.md +998 -0
  14. package/Codex/Skills/CN/skills/helloagents/analyze/SKILL.md +187 -0
  15. package/Codex/Skills/CN/skills/helloagents/design/SKILL.md +261 -0
  16. package/Codex/Skills/CN/skills/helloagents/develop/SKILL.md +352 -0
  17. package/Codex/Skills/CN/skills/helloagents/kb/SKILL.md +249 -0
  18. package/Codex/Skills/CN/skills/helloagents/templates/SKILL.md +451 -0
  19. package/Codex/Skills/EN/AGENTS.md +998 -0
  20. package/Codex/Skills/EN/skills/helloagents/analyze/SKILL.md +187 -0
  21. package/Codex/Skills/EN/skills/helloagents/design/SKILL.md +261 -0
  22. package/Codex/Skills/EN/skills/helloagents/develop/SKILL.md +352 -0
  23. package/Codex/Skills/EN/skills/helloagents/kb/SKILL.md +249 -0
  24. package/Codex/Skills/EN/skills/helloagents/templates/SKILL.md +451 -0
  25. package/README.md +840 -0
  26. package/bin/cli.js +85 -0
  27. package/lib/args.js +106 -0
  28. package/lib/backup.js +81 -0
  29. package/lib/conflict.js +118 -0
  30. package/lib/copy.js +125 -0
  31. package/lib/defaults.js +47 -0
  32. package/lib/index.js +297 -0
  33. package/lib/output.js +220 -0
  34. package/lib/prompts.js +173 -0
  35. package/lib/utils.js +225 -0
  36. package/package.json +38 -0
package/lib/index.js ADDED
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Index - Main flow implementation
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const fsPromises = require('fs').promises;
9
+
10
+ const { getHomeDir, getSourcePaths, getTargetPaths, fileExists, dirExists, readFileContent, rmrf, ensureDir } = require('./utils');
11
+ const { detectDefaultPlatform, getDefaultLang } = require('./defaults');
12
+ const { createReadline, askPlatform, askLanguage, askConfigConflict, askSkillsConflict } = require('./prompts');
13
+ const { backupFile, backupDir } = require('./backup');
14
+ const { checkConfigFile, checkSkillsDir, checkNewFile, backupNewFileIfDifferent, resolveConflict } = require('./conflict');
15
+ const { copyFile, copyDir, writeNewFile, listFiles } = require('./copy');
16
+ const { createOutput, printWelcome, printSummary, printDryRunSummary, printRecoveryInstructions, info, success, warn, error, log, newline, header, divider, operation } = require('./output');
17
+
18
+ /**
19
+ * Main run function
20
+ * @param {Object} options - CLI options
21
+ * @param {Object} deps - Dependencies for testing
22
+ * @returns {Promise<void>}
23
+ */
24
+ async function run(options, deps = {}) {
25
+ const fsModule = deps.fs || fsPromises;
26
+ const homeDir = getHomeDir(deps);
27
+ const output = deps.output || createOutput(deps);
28
+
29
+ // Track stats and backups
30
+ const stats = { write: 0, backup: 0, newFile: 0, skip: 0 };
31
+ const backups = [];
32
+
33
+ // Readline interface (only created if needed)
34
+ let rl = null;
35
+
36
+ try {
37
+ // Print welcome message
38
+ output.printWelcome();
39
+
40
+ // Determine platform
41
+ let platform = options.platform;
42
+ let platformReason = '';
43
+
44
+ if (!platform) {
45
+ if (options.yes) {
46
+ // Auto-detect in -y mode
47
+ const detected = await detectDefaultPlatform(deps);
48
+ platform = detected.platform;
49
+ platformReason = detected.reason;
50
+ output.info(`[默认] ${platformReason},使用 ${platform === 'claude' ? 'Claude Code' : 'Codex'}`);
51
+ } else {
52
+ // Interactive mode
53
+ rl = createReadline();
54
+ const detected = await detectDefaultPlatform(deps);
55
+ platform = await askPlatform(rl, detected.platform, detected.reason);
56
+ }
57
+ } else {
58
+ output.info(`使用指定平台: ${platform === 'claude' ? 'Claude Code' : 'Codex'}`);
59
+ }
60
+
61
+ // Determine language
62
+ let lang = options.lang;
63
+
64
+ if (!lang) {
65
+ if (options.yes) {
66
+ // Use default in -y mode
67
+ const defaultLang = getDefaultLang();
68
+ lang = defaultLang.lang;
69
+ output.info(`[默认] 使用语言: ${lang === 'cn' ? '中文' : 'English'}`);
70
+ } else {
71
+ // Interactive mode
72
+ if (!rl) {
73
+ rl = createReadline();
74
+ }
75
+ const defaultLang = getDefaultLang();
76
+ lang = await askLanguage(rl, defaultLang.lang);
77
+ }
78
+ } else {
79
+ output.info(`使用指定语言: ${lang === 'cn' ? '中文' : 'English'}`);
80
+ }
81
+
82
+ // Get source and target paths
83
+ const sourcePaths = getSourcePaths(platform, lang);
84
+ const targetPaths = getTargetPaths(platform, deps);
85
+
86
+ // Validate source files exist
87
+ const sourceConfigExists = await fileExists(sourcePaths.configFile, deps);
88
+ const sourceSkillsExists = await dirExists(sourcePaths.skillsDir, deps);
89
+
90
+ if (!sourceConfigExists) {
91
+ throw new Error(`源配置文件不存在: ${sourcePaths.configFile}`);
92
+ }
93
+
94
+ if (!sourceSkillsExists) {
95
+ throw new Error(`源技能目录不存在: ${sourcePaths.skillsDir}`);
96
+ }
97
+
98
+ // Ensure target directory exists
99
+ await ensureDir(targetPaths.targetDir, deps);
100
+
101
+ // Dry run mode
102
+ if (options.dryRun) {
103
+ output.header('[Dry Run] 以下操作不会实际执行:');
104
+
105
+ // Check config file
106
+ if (!options.skillsOnly) {
107
+ const configExists = await checkConfigFile(targetPaths.configFile, deps);
108
+ const configFileName = platform === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
109
+
110
+ if (configExists) {
111
+ if (options.overwrite) {
112
+ output.operation(sourcePaths.configFile, targetPaths.configFile, '备份后覆盖');
113
+ stats.backup++;
114
+ stats.write++;
115
+ } else {
116
+ output.operation(sourcePaths.configFile, targetPaths.newFile, `生成 .helloagents.new(${configFileName} 已存在)`);
117
+ stats.newFile++;
118
+ }
119
+ } else {
120
+ output.operation(sourcePaths.configFile, targetPaths.configFile, '写入');
121
+ stats.write++;
122
+ }
123
+ }
124
+
125
+ // Check skills directory
126
+ const skillsExists = await checkSkillsDir(targetPaths.skillsDir, deps);
127
+
128
+ if (skillsExists) {
129
+ if (options.noBackup) {
130
+ output.operation(sourcePaths.skillsDir, targetPaths.skillsDir, '直接覆盖');
131
+ } else {
132
+ output.operation(sourcePaths.skillsDir, targetPaths.skillsDir, '备份后覆盖');
133
+ stats.backup++;
134
+ }
135
+ stats.write++;
136
+ } else {
137
+ output.operation(sourcePaths.skillsDir, targetPaths.skillsDir, '写入');
138
+ stats.write++;
139
+ }
140
+
141
+ output.printDryRunSummary(stats);
142
+ return;
143
+ }
144
+
145
+ // Process config file (unless --skills-only)
146
+ if (!options.skillsOnly) {
147
+ const configExists = await checkConfigFile(targetPaths.configFile, deps);
148
+ const configFileName = platform === 'claude' ? 'CLAUDE.md' : 'AGENTS.md';
149
+
150
+ if (configExists) {
151
+ // Determine action
152
+ let action;
153
+ if (options.yes) {
154
+ action = resolveConflict(options, 'config');
155
+ } else {
156
+ if (!rl) {
157
+ rl = createReadline();
158
+ }
159
+ action = await askConfigConflict(rl, configFileName);
160
+ }
161
+
162
+ switch (action) {
163
+ case 'new': {
164
+ // Check if .new file already exists
165
+ const sourceContent = await readFileContent(sourcePaths.configFile, deps);
166
+ const newFileStatus = await checkNewFile(targetPaths.newFile, sourceContent, deps);
167
+
168
+ if (newFileStatus.exists) {
169
+ if (newFileStatus.sameContent) {
170
+ output.info(`.helloagents.new 已是最新,跳过`);
171
+ stats.skip++;
172
+ } else {
173
+ // Backup existing .new file
174
+ const newBackupPath = await backupNewFileIfDifferent(targetPaths.newFile, deps);
175
+ backups.push(newBackupPath);
176
+ output.info(`已备份旧的 .helloagents.new 至 ${path.basename(newBackupPath)}`);
177
+ stats.backup++;
178
+
179
+ // Write new .new file
180
+ await writeNewFile(sourcePaths.configFile, targetPaths.newFile, deps);
181
+ output.success(`已生成 .helloagents.new`);
182
+ stats.newFile++;
183
+ }
184
+ } else {
185
+ await writeNewFile(sourcePaths.configFile, targetPaths.newFile, deps);
186
+ output.success(`已生成 .helloagents.new`);
187
+ stats.newFile++;
188
+ }
189
+ break;
190
+ }
191
+
192
+ case 'backup': {
193
+ const backupPath = await backupFile(targetPaths.configFile, deps);
194
+ backups.push(backupPath);
195
+ output.info(`已备份 ${configFileName} 至 ${path.basename(backupPath)}`);
196
+ stats.backup++;
197
+
198
+ await copyFile(sourcePaths.configFile, targetPaths.configFile, deps);
199
+ output.success(`已覆盖 ${configFileName}`);
200
+ stats.write++;
201
+ break;
202
+ }
203
+
204
+ case 'overwrite': {
205
+ await copyFile(sourcePaths.configFile, targetPaths.configFile, deps);
206
+ output.success(`已覆盖 ${configFileName}`);
207
+ stats.write++;
208
+ break;
209
+ }
210
+
211
+ case 'skip': {
212
+ output.info(`跳过 ${configFileName}`);
213
+ stats.skip++;
214
+ break;
215
+ }
216
+ }
217
+ } else {
218
+ // No conflict, just copy
219
+ await copyFile(sourcePaths.configFile, targetPaths.configFile, deps);
220
+ output.success(`已写入 ${configFileName}`);
221
+ stats.write++;
222
+ }
223
+ }
224
+
225
+ // Process skills directory
226
+ const skillsExists = await checkSkillsDir(targetPaths.skillsDir, deps);
227
+
228
+ if (skillsExists) {
229
+ // Determine action
230
+ let action;
231
+ if (options.yes) {
232
+ action = resolveConflict(options, 'skills');
233
+ } else {
234
+ if (!rl) {
235
+ rl = createReadline();
236
+ }
237
+ action = await askSkillsConflict(rl);
238
+ }
239
+
240
+ switch (action) {
241
+ case 'backup': {
242
+ const backupPath = await backupDir(targetPaths.skillsDir, deps);
243
+ backups.push(backupPath);
244
+ output.info(`已备份 skills/helloagents 至 ${path.basename(backupPath)}`);
245
+ stats.backup++;
246
+
247
+ // Remove old directory and copy new
248
+ await rmrf(targetPaths.skillsDir, deps);
249
+ await copyDir(sourcePaths.skillsDir, targetPaths.skillsDir, deps);
250
+ output.success(`已更新 skills/helloagents`);
251
+ stats.write++;
252
+ break;
253
+ }
254
+
255
+ case 'overwrite': {
256
+ await rmrf(targetPaths.skillsDir, deps);
257
+ await copyDir(sourcePaths.skillsDir, targetPaths.skillsDir, deps);
258
+ output.success(`已覆盖 skills/helloagents`);
259
+ stats.write++;
260
+ break;
261
+ }
262
+
263
+ case 'skip': {
264
+ output.info(`跳过 skills/helloagents`);
265
+ stats.skip++;
266
+ break;
267
+ }
268
+ }
269
+ } else {
270
+ // No conflict, ensure parent directory exists and copy
271
+ await ensureDir(path.dirname(targetPaths.skillsDir), deps);
272
+ await copyDir(sourcePaths.skillsDir, targetPaths.skillsDir, deps);
273
+ output.success(`已写入 skills/helloagents`);
274
+ stats.write++;
275
+ }
276
+
277
+ // Print summary
278
+ output.printSummary(stats, backups);
279
+
280
+ } catch (err) {
281
+ // Print recovery instructions if we have backups
282
+ if (backups.length > 0) {
283
+ output.printRecoveryInstructions(err.message, backups, options.platform || 'claude');
284
+ }
285
+ throw err;
286
+
287
+ } finally {
288
+ // Close readline if opened
289
+ if (rl) {
290
+ rl.close();
291
+ }
292
+ }
293
+ }
294
+
295
+ module.exports = {
296
+ run,
297
+ };
package/lib/output.js ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Output - Formatted output with ANSI colors
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ // Check if colors should be disabled
8
+ const NO_COLOR = process.env.NO_COLOR !== undefined || process.argv.includes('--no-color');
9
+
10
+ // ANSI color codes
11
+ const colors = {
12
+ reset: NO_COLOR ? '' : '\x1b[0m',
13
+ bold: NO_COLOR ? '' : '\x1b[1m',
14
+ dim: NO_COLOR ? '' : '\x1b[2m',
15
+
16
+ // Foreground colors
17
+ red: NO_COLOR ? '' : '\x1b[31m',
18
+ green: NO_COLOR ? '' : '\x1b[32m',
19
+ yellow: NO_COLOR ? '' : '\x1b[33m',
20
+ blue: NO_COLOR ? '' : '\x1b[34m',
21
+ magenta: NO_COLOR ? '' : '\x1b[35m',
22
+ cyan: NO_COLOR ? '' : '\x1b[36m',
23
+ white: NO_COLOR ? '' : '\x1b[37m',
24
+ };
25
+
26
+ // Symbols
27
+ const symbols = {
28
+ success: NO_COLOR ? '[OK]' : '✔',
29
+ error: NO_COLOR ? '[ERROR]' : '✖',
30
+ warning: NO_COLOR ? '[WARN]' : '⚠',
31
+ info: NO_COLOR ? '[INFO]' : 'ℹ',
32
+ arrow: NO_COLOR ? '->' : '→',
33
+ bullet: NO_COLOR ? '*' : '•',
34
+ };
35
+
36
+ /**
37
+ * Create output functions with injectable stdout
38
+ * @param {Object} deps - Dependencies
39
+ * @returns {Object} Output functions
40
+ */
41
+ function createOutput(deps = {}) {
42
+ const stdout = deps.stdout || process.stdout;
43
+ const stderr = deps.stderr || process.stderr;
44
+
45
+ function write(stream, message) {
46
+ stream.write(message + '\n');
47
+ }
48
+
49
+ return {
50
+ /**
51
+ * Print success message
52
+ * @param {string} message
53
+ */
54
+ success(message) {
55
+ write(stdout, `${colors.green}${symbols.success}${colors.reset} ${message}`);
56
+ },
57
+
58
+ /**
59
+ * Print error message
60
+ * @param {string} message
61
+ */
62
+ error(message) {
63
+ write(stderr, `${colors.red}${symbols.error}${colors.reset} ${message}`);
64
+ },
65
+
66
+ /**
67
+ * Print warning message
68
+ * @param {string} message
69
+ */
70
+ warn(message) {
71
+ write(stdout, `${colors.yellow}${symbols.warning}${colors.reset} ${message}`);
72
+ },
73
+
74
+ /**
75
+ * Print info message
76
+ * @param {string} message
77
+ */
78
+ info(message) {
79
+ write(stdout, `${colors.cyan}${symbols.info}${colors.reset} ${message}`);
80
+ },
81
+
82
+ /**
83
+ * Print plain message
84
+ * @param {string} message
85
+ */
86
+ log(message) {
87
+ write(stdout, message);
88
+ },
89
+
90
+ /**
91
+ * Print empty line
92
+ */
93
+ newline() {
94
+ write(stdout, '');
95
+ },
96
+
97
+ /**
98
+ * Print header
99
+ * @param {string} title
100
+ */
101
+ header(title) {
102
+ write(stdout, `\n${colors.bold}${colors.cyan}${title}${colors.reset}\n`);
103
+ },
104
+
105
+ /**
106
+ * Print divider
107
+ */
108
+ divider() {
109
+ write(stdout, `${colors.dim}${'─'.repeat(40)}${colors.reset}`);
110
+ },
111
+
112
+ /**
113
+ * Print operation item
114
+ * @param {string} source
115
+ * @param {string} target
116
+ * @param {string} action
117
+ */
118
+ operation(source, target, action) {
119
+ write(stdout, `\n源: ${colors.cyan}${source}${colors.reset}`);
120
+ write(stdout, ` ${symbols.arrow} 目标: ${colors.cyan}${target}${colors.reset}`);
121
+ write(stdout, ` ${symbols.arrow} 操作: ${action}`);
122
+ },
123
+
124
+ /**
125
+ * Print dry-run summary
126
+ * @param {Object} stats - { write, backup, newFile, skip }
127
+ */
128
+ printDryRunSummary(stats) {
129
+ write(stdout, '');
130
+ write(stdout, `${colors.dim}${'─'.repeat(40)}${colors.reset}`);
131
+ write(stdout, `汇总: 写入 ${stats.write || 0} | 备份 ${stats.backup || 0} | 生成.new ${stats.newFile || 0} | 跳过 ${stats.skip || 0}`);
132
+ write(stdout, '');
133
+ write(stdout, `${colors.yellow}${symbols.warning}${colors.reset} Dry Run 模式:不会做任何写入`);
134
+ },
135
+
136
+ /**
137
+ * Print final summary
138
+ * @param {Object} stats - { write, backup, newFile, skip }
139
+ * @param {Array} backups - List of backup paths
140
+ */
141
+ printSummary(stats, backups = []) {
142
+ write(stdout, '');
143
+ write(stdout, `${colors.dim}${'─'.repeat(40)}${colors.reset}`);
144
+ write(stdout, `${colors.green}${symbols.success}${colors.reset} 安装完成!`);
145
+ write(stdout, '');
146
+ write(stdout, `汇总: 写入 ${stats.write || 0} | 备份 ${stats.backup || 0} | 生成.new ${stats.newFile || 0} | 跳过 ${stats.skip || 0}`);
147
+
148
+ if (backups.length > 0) {
149
+ write(stdout, '');
150
+ write(stdout, `${colors.cyan}已创建的备份:${colors.reset}`);
151
+ for (const backup of backups) {
152
+ write(stdout, ` ${symbols.bullet} ${backup}`);
153
+ }
154
+ }
155
+ },
156
+
157
+ /**
158
+ * Print welcome message
159
+ */
160
+ printWelcome() {
161
+ write(stdout, '');
162
+ write(stdout, `${colors.bold}${colors.cyan}HelloAGENTS${colors.reset} - AI编程模块化技能系统`);
163
+ write(stdout, `${colors.dim}配置工具 v1.0.0${colors.reset}`);
164
+ write(stdout, '');
165
+ },
166
+
167
+ /**
168
+ * Print recovery instructions on failure
169
+ * @param {string} errorMessage
170
+ * @param {Array} backups - List of backup paths
171
+ * @param {string} platform - 'claude' or 'codex'
172
+ */
173
+ printRecoveryInstructions(errorMessage, backups, platform) {
174
+ const configDir = platform === 'claude' ? '.claude' : '.codex';
175
+
176
+ write(stderr, '');
177
+ write(stderr, `${colors.red}${symbols.error} 复制失败: ${errorMessage}${colors.reset}`);
178
+
179
+ if (backups.length > 0) {
180
+ write(stderr, '');
181
+ write(stderr, `${colors.cyan}已创建的备份:${colors.reset}`);
182
+ for (const backup of backups) {
183
+ write(stderr, ` ${backup}`);
184
+ }
185
+
186
+ write(stderr, '');
187
+ write(stderr, `${colors.cyan}恢复命令(如需回滚):${colors.reset}`);
188
+ write(stderr, '');
189
+ write(stderr, `# macOS/Linux`);
190
+ for (const backup of backups) {
191
+ const original = backup.replace(/\.backup-\d{8}-\d{6}\/?$/, '');
192
+ const name = original.split('/').pop() || original.split('\\').pop();
193
+ write(stderr, `rm -rf ${original}`);
194
+ write(stderr, `mv ${backup} ${original}`);
195
+ }
196
+ write(stderr, '');
197
+ write(stderr, `# Windows PowerShell`);
198
+ for (const backup of backups) {
199
+ const original = backup.replace(/\.backup-\d{8}-\d{6}\/?$/, '');
200
+ const name = original.split('/').pop() || original.split('\\').pop();
201
+ write(stderr, `Remove-Item "${original}" -Recurse -Force`);
202
+ write(stderr, `Rename-Item "${backup}" "${name}"`);
203
+ }
204
+ }
205
+
206
+ write(stderr, '');
207
+ write(stderr, `请修复问题后重新运行,或使用上述命令手动恢复。`);
208
+ },
209
+ };
210
+ }
211
+
212
+ // Default instance
213
+ const defaultOutput = createOutput();
214
+
215
+ module.exports = {
216
+ colors,
217
+ symbols,
218
+ createOutput,
219
+ ...defaultOutput,
220
+ };
package/lib/prompts.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Prompts - Interactive prompts using native readline
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const readline = require('readline');
8
+
9
+ /**
10
+ * Create readline interface
11
+ * @returns {readline.Interface}
12
+ */
13
+ function createReadline() {
14
+ return readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Ask a question and get answer
22
+ * @param {readline.Interface} rl - Readline interface
23
+ * @param {string} question - Question to ask
24
+ * @returns {Promise<string>}
25
+ */
26
+ function ask(rl, question) {
27
+ return new Promise((resolve) => {
28
+ rl.question(question, (answer) => {
29
+ resolve(answer.trim());
30
+ });
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Ask for platform selection
36
+ * @param {readline.Interface} rl - Readline interface
37
+ * @param {string} defaultPlatform - Default platform
38
+ * @param {string} reason - Reason for default
39
+ * @returns {Promise<string>} 'claude' or 'codex'
40
+ */
41
+ async function askPlatform(rl, defaultPlatform = 'claude', reason = '') {
42
+ const defaultNum = defaultPlatform === 'claude' ? '1' : '2';
43
+ const reasonText = reason ? ` (${reason})` : '';
44
+
45
+ console.log('');
46
+ console.log('选择目标平台:');
47
+ console.log(` 1. Claude Code${defaultPlatform === 'claude' ? ` [默认]${reasonText}` : ''}`);
48
+ console.log(` 2. Codex${defaultPlatform === 'codex' ? ` [默认]${reasonText}` : ''}`);
49
+ console.log('');
50
+
51
+ const answer = await ask(rl, `请输入选项 (1-2) [${defaultNum}]: `);
52
+
53
+ if (answer === '' || answer === defaultNum) {
54
+ return defaultPlatform;
55
+ }
56
+
57
+ if (answer === '1') {
58
+ return 'claude';
59
+ }
60
+
61
+ if (answer === '2') {
62
+ return 'codex';
63
+ }
64
+
65
+ // Invalid input, ask again
66
+ console.log('无效输入,请输入 1 或 2');
67
+ return askPlatform(rl, defaultPlatform, reason);
68
+ }
69
+
70
+ /**
71
+ * Ask for language selection
72
+ * @param {readline.Interface} rl - Readline interface
73
+ * @param {string} defaultLang - Default language
74
+ * @returns {Promise<string>} 'cn' or 'en'
75
+ */
76
+ async function askLanguage(rl, defaultLang = 'cn') {
77
+ const defaultNum = defaultLang === 'cn' ? '1' : '2';
78
+
79
+ console.log('');
80
+ console.log('选择语言版本:');
81
+ console.log(` 1. 中文${defaultLang === 'cn' ? ' [默认]' : ''}`);
82
+ console.log(` 2. English${defaultLang === 'en' ? ' [默认]' : ''}`);
83
+ console.log('');
84
+
85
+ const answer = await ask(rl, `请输入选项 (1-2) [${defaultNum}]: `);
86
+
87
+ if (answer === '' || answer === defaultNum) {
88
+ return defaultLang;
89
+ }
90
+
91
+ if (answer === '1') {
92
+ return 'cn';
93
+ }
94
+
95
+ if (answer === '2') {
96
+ return 'en';
97
+ }
98
+
99
+ // Invalid input, ask again
100
+ console.log('无效输入,请输入 1 或 2');
101
+ return askLanguage(rl, defaultLang);
102
+ }
103
+
104
+ /**
105
+ * Ask for config file conflict resolution
106
+ * @param {readline.Interface} rl - Readline interface
107
+ * @param {string} fileName - Name of the config file
108
+ * @returns {Promise<string>} 'new' | 'backup' | 'overwrite' | 'skip'
109
+ */
110
+ async function askConfigConflict(rl, fileName) {
111
+ console.log('');
112
+ console.log(`检测到 ${fileName} 已存在,请选择处理方式:`);
113
+ console.log(' 1. 生成 .helloagents.new 文件(保留原文件)[默认]');
114
+ console.log(' 2. 备份后覆盖');
115
+ console.log(' 3. 直接覆盖(不备份)');
116
+ console.log(' 4. 跳过');
117
+ console.log('');
118
+
119
+ const answer = await ask(rl, '请输入选项 (1-4) [1]: ');
120
+
121
+ switch (answer) {
122
+ case '':
123
+ case '1':
124
+ return 'new';
125
+ case '2':
126
+ return 'backup';
127
+ case '3':
128
+ return 'overwrite';
129
+ case '4':
130
+ return 'skip';
131
+ default:
132
+ console.log('无效输入,请输入 1-4');
133
+ return askConfigConflict(rl, fileName);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Ask for skills directory conflict resolution
139
+ * @param {readline.Interface} rl - Readline interface
140
+ * @returns {Promise<string>} 'backup' | 'overwrite' | 'skip'
141
+ */
142
+ async function askSkillsConflict(rl) {
143
+ console.log('');
144
+ console.log('检测到 skills/helloagents 目录已存在,请选择处理方式:');
145
+ console.log(' 1. 备份后覆盖 [默认]');
146
+ console.log(' 2. 直接覆盖(不备份)');
147
+ console.log(' 3. 跳过');
148
+ console.log('');
149
+
150
+ const answer = await ask(rl, '请输入选项 (1-3) [1]: ');
151
+
152
+ switch (answer) {
153
+ case '':
154
+ case '1':
155
+ return 'backup';
156
+ case '2':
157
+ return 'overwrite';
158
+ case '3':
159
+ return 'skip';
160
+ default:
161
+ console.log('无效输入,请输入 1-3');
162
+ return askSkillsConflict(rl);
163
+ }
164
+ }
165
+
166
+ module.exports = {
167
+ createReadline,
168
+ ask,
169
+ askPlatform,
170
+ askLanguage,
171
+ askConfigConflict,
172
+ askSkillsConflict,
173
+ };