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.
- package/Claude/Skills/CN/CLAUDE.md +998 -0
- package/Claude/Skills/CN/skills/helloagents/analyze/SKILL.md +187 -0
- package/Claude/Skills/CN/skills/helloagents/design/SKILL.md +261 -0
- package/Claude/Skills/CN/skills/helloagents/develop/SKILL.md +352 -0
- package/Claude/Skills/CN/skills/helloagents/kb/SKILL.md +249 -0
- package/Claude/Skills/CN/skills/helloagents/templates/SKILL.md +451 -0
- package/Claude/Skills/EN/CLAUDE.md +998 -0
- package/Claude/Skills/EN/skills/helloagents/analyze/SKILL.md +187 -0
- package/Claude/Skills/EN/skills/helloagents/design/SKILL.md +261 -0
- package/Claude/Skills/EN/skills/helloagents/develop/SKILL.md +352 -0
- package/Claude/Skills/EN/skills/helloagents/kb/SKILL.md +249 -0
- package/Claude/Skills/EN/skills/helloagents/templates/SKILL.md +451 -0
- package/Codex/Skills/CN/AGENTS.md +998 -0
- package/Codex/Skills/CN/skills/helloagents/analyze/SKILL.md +187 -0
- package/Codex/Skills/CN/skills/helloagents/design/SKILL.md +261 -0
- package/Codex/Skills/CN/skills/helloagents/develop/SKILL.md +352 -0
- package/Codex/Skills/CN/skills/helloagents/kb/SKILL.md +249 -0
- package/Codex/Skills/CN/skills/helloagents/templates/SKILL.md +451 -0
- package/Codex/Skills/EN/AGENTS.md +998 -0
- package/Codex/Skills/EN/skills/helloagents/analyze/SKILL.md +187 -0
- package/Codex/Skills/EN/skills/helloagents/design/SKILL.md +261 -0
- package/Codex/Skills/EN/skills/helloagents/develop/SKILL.md +352 -0
- package/Codex/Skills/EN/skills/helloagents/kb/SKILL.md +249 -0
- package/Codex/Skills/EN/skills/helloagents/templates/SKILL.md +451 -0
- package/README.md +840 -0
- package/bin/cli.js +85 -0
- package/lib/args.js +106 -0
- package/lib/backup.js +81 -0
- package/lib/conflict.js +118 -0
- package/lib/copy.js +125 -0
- package/lib/defaults.js +47 -0
- package/lib/index.js +297 -0
- package/lib/output.js +220 -0
- package/lib/prompts.js +173 -0
- package/lib/utils.js +225 -0
- 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
|
+
};
|