create-byan-agent 2.7.0 → 2.7.2
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/README.md +151 -207
- package/bin/create-byan-agent-v2.js +1 -1
- package/lib/domain-questions.js +235 -0
- package/lib/errors.js +61 -0
- package/lib/exit-codes.js +54 -0
- package/lib/phase2-chat.js +534 -0
- package/lib/platforms/claude-code.js +196 -0
- package/lib/platforms/codex.js +92 -0
- package/lib/platforms/copilot-cli.js +123 -0
- package/lib/platforms/index.js +14 -0
- package/lib/platforms/vscode.js +51 -0
- package/lib/project-agents-generator.js +238 -0
- package/lib/utils/config-loader.js +79 -0
- package/lib/utils/file-utils.js +104 -0
- package/lib/utils/git-detector.js +35 -0
- package/lib/utils/logger.js +64 -0
- package/lib/utils/node-detector.js +58 -0
- package/lib/utils/os-detector.js +74 -0
- package/lib/utils/yaml-utils.js +87 -0
- package/lib/yanstaller/agent-launcher.js +348 -0
- package/lib/yanstaller/backuper.js +108 -0
- package/lib/yanstaller/detector.js +141 -0
- package/lib/yanstaller/index.js +139 -0
- package/lib/yanstaller/installer.js +140 -0
- package/lib/yanstaller/interviewer.js +88 -0
- package/lib/yanstaller/platform-selector.js +328 -0
- package/lib/yanstaller/recommender.js +102 -0
- package/lib/yanstaller/troubleshooter.js +89 -0
- package/lib/yanstaller/validator.js +198 -0
- package/lib/yanstaller/wizard.js +109 -0
- package/package.json +3 -1
- package/setup-turbo-whisper.js +687 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAML Utilities
|
|
3
|
+
*
|
|
4
|
+
* Wrapper around js-yaml for YAML parsing/dumping.
|
|
5
|
+
*
|
|
6
|
+
* @module utils/yaml-utils
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const fileUtils = require('./file-utils');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse YAML string
|
|
14
|
+
*
|
|
15
|
+
* @param {string} yamlString - YAML string
|
|
16
|
+
* @returns {Object}
|
|
17
|
+
*/
|
|
18
|
+
function parse(yamlString) {
|
|
19
|
+
return yaml.load(yamlString);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dump object to YAML string
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} obj - Object to dump
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function dump(obj) {
|
|
29
|
+
return yaml.dump(obj, {
|
|
30
|
+
indent: 2,
|
|
31
|
+
lineWidth: -1
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read YAML file
|
|
37
|
+
*
|
|
38
|
+
* @param {string} filePath - YAML file path
|
|
39
|
+
* @returns {Promise<Object>}
|
|
40
|
+
*/
|
|
41
|
+
async function readYAML(filePath) {
|
|
42
|
+
const content = await fileUtils.readFile(filePath);
|
|
43
|
+
return parse(content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write YAML file
|
|
48
|
+
*
|
|
49
|
+
* @param {string} filePath - YAML file path
|
|
50
|
+
* @param {Object} data - Data to write
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async function writeYAML(filePath, data) {
|
|
54
|
+
const yamlString = dump(data);
|
|
55
|
+
await fileUtils.writeFile(filePath, yamlString);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract YAML frontmatter from markdown
|
|
60
|
+
*
|
|
61
|
+
* @param {string} markdownContent - Markdown content
|
|
62
|
+
* @returns {{frontmatter: Object | null, content: string}}
|
|
63
|
+
*/
|
|
64
|
+
function extractFrontmatter(markdownContent) {
|
|
65
|
+
const frontmatterRegex = /^---\n([\s\S]+?)\n---\n([\s\S]*)$/;
|
|
66
|
+
const match = markdownContent.match(frontmatterRegex);
|
|
67
|
+
|
|
68
|
+
if (!match) {
|
|
69
|
+
return {
|
|
70
|
+
frontmatter: null,
|
|
71
|
+
content: markdownContent
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
frontmatter: parse(match[1]),
|
|
77
|
+
content: match[2]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
parse,
|
|
83
|
+
dump,
|
|
84
|
+
readYAML,
|
|
85
|
+
writeYAML,
|
|
86
|
+
extractFrontmatter
|
|
87
|
+
};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AGENT LAUNCHER Module
|
|
3
|
+
*
|
|
4
|
+
* Launches specialist agents using native platform commands.
|
|
5
|
+
* Each platform has its own invocation syntax.
|
|
6
|
+
*
|
|
7
|
+
* @module yanstaller/agent-launcher
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync, spawn } = require('child_process');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} LaunchOptions
|
|
15
|
+
* @property {string} agent - Agent name (e.g., 'claude', 'marc')
|
|
16
|
+
* @property {string} platform - Platform ID (e.g., 'copilot-cli', 'claude')
|
|
17
|
+
* @property {string} [prompt] - Initial prompt/action
|
|
18
|
+
* @property {string} [model] - Model to use
|
|
19
|
+
* @property {Object} [config] - Additional config
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} LaunchResult
|
|
24
|
+
* @property {boolean} success
|
|
25
|
+
* @property {string} method - How agent was launched
|
|
26
|
+
* @property {string} [output] - Command output
|
|
27
|
+
* @property {string} [error] - Error message if failed
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Platform-specific launch configurations
|
|
32
|
+
*/
|
|
33
|
+
const LAUNCH_CONFIGS = {
|
|
34
|
+
'copilot-cli': {
|
|
35
|
+
command: 'gh',
|
|
36
|
+
args: (agent, options) => {
|
|
37
|
+
const args = ['copilot'];
|
|
38
|
+
|
|
39
|
+
// Use @agent syntax if available
|
|
40
|
+
if (agent) {
|
|
41
|
+
args.push(`@bmad-agent-${agent}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options.prompt) {
|
|
45
|
+
args.push(options.prompt);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return args;
|
|
49
|
+
},
|
|
50
|
+
checkAvailable: () => {
|
|
51
|
+
try {
|
|
52
|
+
execSync('which gh', { stdio: 'ignore' });
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
'claude': {
|
|
61
|
+
command: 'claude',
|
|
62
|
+
args: (agent, options = {}) => {
|
|
63
|
+
const args = [];
|
|
64
|
+
|
|
65
|
+
// Agent specification
|
|
66
|
+
if (agent) {
|
|
67
|
+
args.push('--agent', agent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Model selection
|
|
71
|
+
if (options.model) {
|
|
72
|
+
args.push('--model', options.model);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// System prompt for context (before positional prompt)
|
|
76
|
+
if (options.systemPrompt) {
|
|
77
|
+
args.push('--system-prompt', options.systemPrompt);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// MCP config if needed
|
|
81
|
+
if (options.mcpConfig) {
|
|
82
|
+
args.push('--mcp-config', options.mcpConfig);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Prompt as POSITIONAL argument (not --prompt)
|
|
86
|
+
// Must come AFTER all flags
|
|
87
|
+
if (options.prompt) {
|
|
88
|
+
args.push(options.prompt);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return args;
|
|
92
|
+
},
|
|
93
|
+
checkAvailable: () => {
|
|
94
|
+
try {
|
|
95
|
+
execSync('which claude', { stdio: 'ignore' });
|
|
96
|
+
return true;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
'codex': {
|
|
104
|
+
command: 'codex',
|
|
105
|
+
args: (agent, options = {}) => {
|
|
106
|
+
const args = [];
|
|
107
|
+
|
|
108
|
+
// Codex uses "skills" not "agents"
|
|
109
|
+
// Format: codex skill <skill-name> [prompt]
|
|
110
|
+
|
|
111
|
+
if (agent) {
|
|
112
|
+
args.push('skill', `bmad-${agent}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Prompt as positional argument
|
|
116
|
+
if (options.prompt) {
|
|
117
|
+
args.push(options.prompt);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Model selection (if Codex supports it)
|
|
121
|
+
if (options.model) {
|
|
122
|
+
args.push('--model', options.model);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return args;
|
|
126
|
+
},
|
|
127
|
+
checkAvailable: () => {
|
|
128
|
+
try {
|
|
129
|
+
execSync('which codex', { stdio: 'ignore' });
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Launch agent using native platform command
|
|
140
|
+
*
|
|
141
|
+
* @param {LaunchOptions} options - Launch options
|
|
142
|
+
* @returns {Promise<LaunchResult>}
|
|
143
|
+
*/
|
|
144
|
+
async function launch(options) {
|
|
145
|
+
const { agent, platform, prompt, model, config } = options;
|
|
146
|
+
|
|
147
|
+
const platformConfig = LAUNCH_CONFIGS[platform];
|
|
148
|
+
|
|
149
|
+
if (!platformConfig) {
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
method: 'unsupported',
|
|
153
|
+
error: `Platform ${platform} not supported for native launch`
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if platform command is available
|
|
158
|
+
if (!platformConfig.checkAvailable()) {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
method: 'command-not-found',
|
|
162
|
+
error: `Command '${platformConfig.command}' not found in PATH`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Build command arguments
|
|
167
|
+
const args = platformConfig.args(agent, {
|
|
168
|
+
prompt,
|
|
169
|
+
model,
|
|
170
|
+
systemPrompt: config ? config.systemPrompt : undefined,
|
|
171
|
+
mcpConfig: config ? config.mcpConfig : undefined
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const fullCommand = `${platformConfig.command} ${args.join(' ')}`;
|
|
175
|
+
|
|
176
|
+
logger.info(`Launching agent via: ${fullCommand}`);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
// Launch in interactive mode (inherit stdio)
|
|
180
|
+
const result = spawn(platformConfig.command, args, {
|
|
181
|
+
stdio: 'inherit',
|
|
182
|
+
shell: true
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
result.on('close', (code) => {
|
|
187
|
+
if (code === 0) {
|
|
188
|
+
resolve({
|
|
189
|
+
success: true,
|
|
190
|
+
method: 'native-interactive',
|
|
191
|
+
output: `Agent launched successfully via ${platform}`
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
resolve({
|
|
195
|
+
success: false,
|
|
196
|
+
method: 'native-interactive',
|
|
197
|
+
error: `Agent exited with code ${code}`
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
result.on('error', (error) => {
|
|
203
|
+
resolve({
|
|
204
|
+
success: false,
|
|
205
|
+
method: 'native-interactive',
|
|
206
|
+
error: error.message
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
method: 'native-interactive',
|
|
214
|
+
error: error.message
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Launch agent with specific prompt (non-interactive)
|
|
221
|
+
*
|
|
222
|
+
* Useful for automation or scripted workflows.
|
|
223
|
+
*
|
|
224
|
+
* @param {LaunchOptions} options - Launch options
|
|
225
|
+
* @returns {Promise<LaunchResult>}
|
|
226
|
+
*/
|
|
227
|
+
async function launchWithPrompt(options) {
|
|
228
|
+
const { agent, platform, prompt, model, config } = options;
|
|
229
|
+
|
|
230
|
+
if (!prompt) {
|
|
231
|
+
throw new Error('Prompt required for non-interactive launch');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const platformConfig = LAUNCH_CONFIGS[platform];
|
|
235
|
+
|
|
236
|
+
if (!platformConfig) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
method: 'unsupported',
|
|
240
|
+
error: `Platform ${platform} not supported`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!platformConfig.checkAvailable()) {
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
method: 'command-not-found',
|
|
248
|
+
error: `Command '${platformConfig.command}' not found`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const args = platformConfig.args(agent, {
|
|
253
|
+
prompt,
|
|
254
|
+
model,
|
|
255
|
+
systemPrompt: config ? config.systemPrompt : undefined
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Add --print flag for Claude to get output
|
|
259
|
+
if (platform === 'claude') {
|
|
260
|
+
args.unshift('--print');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const fullCommand = `${platformConfig.command} ${args.join(' ')}`;
|
|
264
|
+
|
|
265
|
+
logger.info(`Executing: ${fullCommand}`);
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const output = execSync(fullCommand, {
|
|
269
|
+
encoding: 'utf8',
|
|
270
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: true,
|
|
275
|
+
method: 'native-print',
|
|
276
|
+
output: output.trim()
|
|
277
|
+
};
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
method: 'native-print',
|
|
282
|
+
error: error.message
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate launch instructions for manual invocation
|
|
289
|
+
*
|
|
290
|
+
* Used as fallback when native launch isn't possible.
|
|
291
|
+
*
|
|
292
|
+
* @param {LaunchOptions} options - Launch options
|
|
293
|
+
* @returns {string} - Human-readable instructions
|
|
294
|
+
*/
|
|
295
|
+
function getLaunchInstructions(options) {
|
|
296
|
+
const { agent, platform, prompt, model } = options;
|
|
297
|
+
|
|
298
|
+
const platformConfig = LAUNCH_CONFIGS[platform];
|
|
299
|
+
|
|
300
|
+
if (!platformConfig) {
|
|
301
|
+
return `Platform ${platform} not yet supported for automated launch.\nPlease activate the agent manually.`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const args = platformConfig.args(agent, { prompt, model });
|
|
305
|
+
const command = `${platformConfig.command} ${args.join(' ')}`;
|
|
306
|
+
|
|
307
|
+
return `
|
|
308
|
+
To activate the agent, run:
|
|
309
|
+
|
|
310
|
+
${command}
|
|
311
|
+
|
|
312
|
+
Or in interactive mode:
|
|
313
|
+
${platformConfig.command}
|
|
314
|
+
Then: @bmad-agent-${agent}
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if platform supports native agent launch
|
|
320
|
+
*
|
|
321
|
+
* @param {string} platform - Platform ID
|
|
322
|
+
* @returns {boolean}
|
|
323
|
+
*/
|
|
324
|
+
function supportsNativeLaunch(platform) {
|
|
325
|
+
const config = LAUNCH_CONFIGS[platform];
|
|
326
|
+
if (!config) return false;
|
|
327
|
+
return config.checkAvailable();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get available platforms for native launch
|
|
332
|
+
*
|
|
333
|
+
* @returns {string[]} - List of platform IDs
|
|
334
|
+
*/
|
|
335
|
+
function getAvailablePlatforms() {
|
|
336
|
+
return Object.keys(LAUNCH_CONFIGS).filter(platform => {
|
|
337
|
+
const config = LAUNCH_CONFIGS[platform];
|
|
338
|
+
return config.checkAvailable();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = {
|
|
343
|
+
launch,
|
|
344
|
+
launchWithPrompt,
|
|
345
|
+
getLaunchInstructions,
|
|
346
|
+
supportsNativeLaunch,
|
|
347
|
+
getAvailablePlatforms
|
|
348
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BACKUPER Module
|
|
3
|
+
*
|
|
4
|
+
* Backs up and restores _bmad/ directory.
|
|
5
|
+
*
|
|
6
|
+
* Phase 6: 24h development
|
|
7
|
+
*
|
|
8
|
+
* @module yanstaller/backuper
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fileUtils = require('../utils/file-utils');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} BackupResult
|
|
16
|
+
* @property {boolean} success
|
|
17
|
+
* @property {string} backupPath - Path to backup directory
|
|
18
|
+
* @property {number} filesBackedUp
|
|
19
|
+
* @property {number} size - Backup size in bytes
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Backup _bmad/ directory
|
|
24
|
+
*
|
|
25
|
+
* @param {string} bmadPath - Path to _bmad/ directory
|
|
26
|
+
* @returns {Promise<BackupResult>}
|
|
27
|
+
*/
|
|
28
|
+
async function backup(bmadPath) {
|
|
29
|
+
const timestamp = Date.now();
|
|
30
|
+
const backupPath = `${bmadPath}.backup-${timestamp}`;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// TODO: Copy entire _bmad/ to backup path
|
|
34
|
+
// await fileUtils.copy(bmadPath, backupPath);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
backupPath,
|
|
39
|
+
filesBackedUp: 0,
|
|
40
|
+
size: 0
|
|
41
|
+
};
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new BackupError(`Failed to backup ${bmadPath}`, { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Restore from backup
|
|
49
|
+
*
|
|
50
|
+
* @param {string} backupPath - Path to backup directory
|
|
51
|
+
* @param {string} targetPath - Target restoration path
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
async function restore(backupPath, targetPath) {
|
|
55
|
+
// TODO: Remove current _bmad/, copy backup to target
|
|
56
|
+
// await fileUtils.remove(targetPath);
|
|
57
|
+
// await fileUtils.copy(backupPath, targetPath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List available backups
|
|
62
|
+
*
|
|
63
|
+
* @param {string} projectRoot - Project root directory
|
|
64
|
+
* @returns {Promise<string[]>} - Array of backup paths
|
|
65
|
+
*/
|
|
66
|
+
async function listBackups(projectRoot) {
|
|
67
|
+
// TODO: Find all _bmad.backup-* directories
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clean old backups (keep last N)
|
|
73
|
+
*
|
|
74
|
+
* @param {string} projectRoot - Project root directory
|
|
75
|
+
* @param {number} keep - Number of backups to keep
|
|
76
|
+
* @returns {Promise<number>} - Number of backups deleted
|
|
77
|
+
*/
|
|
78
|
+
async function cleanOldBackups(projectRoot, keep = 3) {
|
|
79
|
+
// TODO: Sort by timestamp, delete oldest
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get backup size
|
|
85
|
+
*
|
|
86
|
+
* @param {string} backupPath - Path to backup directory
|
|
87
|
+
* @returns {Promise<number>} - Size in bytes
|
|
88
|
+
*/
|
|
89
|
+
async function getBackupSize(backupPath) {
|
|
90
|
+
// TODO: Recursively calculate directory size
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class BackupError extends Error {
|
|
95
|
+
constructor(message, options) {
|
|
96
|
+
super(message, options);
|
|
97
|
+
this.name = 'BackupError';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
backup,
|
|
103
|
+
restore,
|
|
104
|
+
listBackups,
|
|
105
|
+
cleanOldBackups,
|
|
106
|
+
getBackupSize,
|
|
107
|
+
BackupError
|
|
108
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DETECTOR Module
|
|
3
|
+
*
|
|
4
|
+
* Detects OS, Node.js version, Git, and installed platforms.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1: 40h development
|
|
7
|
+
*
|
|
8
|
+
* @module yanstaller/detector
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const osDetector = require('../utils/os-detector');
|
|
12
|
+
const nodeDetector = require('../utils/node-detector');
|
|
13
|
+
const gitDetector = require('../utils/git-detector');
|
|
14
|
+
const platforms = require('../platforms');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} DetectionResult
|
|
18
|
+
* @property {string} os - 'windows' | 'linux' | 'macos'
|
|
19
|
+
* @property {string} osVersion - e.g., '11' for Windows 11
|
|
20
|
+
* @property {string} nodeVersion - e.g., '18.19.0'
|
|
21
|
+
* @property {boolean} hasGit
|
|
22
|
+
* @property {string} [gitVersion] - e.g., '2.43.0'
|
|
23
|
+
* @property {PlatformInfo[]} platforms - Detected platforms
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Object} PlatformInfo
|
|
28
|
+
* @property {string} name - 'copilot-cli' | 'vscode' | 'claude' | 'codex'
|
|
29
|
+
* @property {boolean} detected
|
|
30
|
+
* @property {string} [path] - Installation path if detected
|
|
31
|
+
* @property {string} [version] - Version if detected
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const logger = require('../utils/logger');
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect full environment
|
|
38
|
+
*
|
|
39
|
+
* Runs parallel detection for speed.
|
|
40
|
+
* Non-blocking: platform detection failures are caught and logged.
|
|
41
|
+
*
|
|
42
|
+
* @returns {Promise<DetectionResult>}
|
|
43
|
+
*/
|
|
44
|
+
async function detect() {
|
|
45
|
+
// Parallel detection for speed (Mantra #7 KISS)
|
|
46
|
+
const [osInfo, nodeVersion, gitInfo] = await Promise.all([
|
|
47
|
+
osDetector.detect(),
|
|
48
|
+
Promise.resolve(nodeDetector.detect()), // Sync wrapped in Promise
|
|
49
|
+
gitDetector.detect()
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
// Platform detection with timeout protection
|
|
53
|
+
const platformNames = ['copilot-cli', 'vscode', 'claude', 'codex'];
|
|
54
|
+
const platformsInfo = await Promise.all(
|
|
55
|
+
platformNames.map(name => detectPlatform(name))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Check if ALL platforms failed
|
|
59
|
+
const allFailed = platformsInfo.every(p => !p.detected);
|
|
60
|
+
if (allFailed) {
|
|
61
|
+
const errors = platformsInfo
|
|
62
|
+
.filter(p => p.error)
|
|
63
|
+
.map(p => `${p.name}: ${p.error}`)
|
|
64
|
+
.join(', ');
|
|
65
|
+
if (errors) {
|
|
66
|
+
logger.warn(`0/4 platforms detected. Errors: [${errors}]`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
os: osInfo.name,
|
|
72
|
+
osVersion: osInfo.version,
|
|
73
|
+
nodeVersion,
|
|
74
|
+
hasGit: gitInfo.installed,
|
|
75
|
+
gitVersion: gitInfo.version,
|
|
76
|
+
platforms: platformsInfo
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if Node.js version meets minimum requirement
|
|
82
|
+
*
|
|
83
|
+
* Handles version suffixes (-beta, -rc1) by stripping them.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} currentVersion - e.g., '18.19.0'
|
|
86
|
+
* @param {string} requiredVersion - e.g., '18.0.0'
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isNodeVersionValid(currentVersion, requiredVersion) {
|
|
90
|
+
return nodeDetector.meetsRequirement(currentVersion, requiredVersion);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect specific platform
|
|
95
|
+
*
|
|
96
|
+
* Non-blocking: errors are caught and returned in result.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} platformName - 'copilot-cli' | 'vscode' | 'claude' | 'codex'
|
|
99
|
+
* @returns {Promise<PlatformInfo>}
|
|
100
|
+
*/
|
|
101
|
+
async function detectPlatform(platformName) {
|
|
102
|
+
const platform = platforms[platformName];
|
|
103
|
+
if (!platform) {
|
|
104
|
+
throw new Error(`Unknown platform: ${platformName}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const detected = await platform.detect();
|
|
109
|
+
|
|
110
|
+
// Handle timeout response format (object with detected + error)
|
|
111
|
+
if (typeof detected === 'object' && 'error' in detected) {
|
|
112
|
+
logger.warn(`Platform ${platformName} detection failed: ${detected.error}`);
|
|
113
|
+
return {
|
|
114
|
+
name: platformName,
|
|
115
|
+
detected: false,
|
|
116
|
+
error: detected.error
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: platformName,
|
|
122
|
+
detected: !!detected,
|
|
123
|
+
path: detected ? platform.getPath() : undefined
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Non-blocking: platform detection failure shouldn't crash detection
|
|
127
|
+
// Error UX: Log warning and include in report for user visibility
|
|
128
|
+
logger.warn(`Platform ${platformName} detection failed: ${error.message}`);
|
|
129
|
+
return {
|
|
130
|
+
name: platformName,
|
|
131
|
+
detected: false,
|
|
132
|
+
error: error.message
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
detect,
|
|
139
|
+
isNodeVersionValid,
|
|
140
|
+
detectPlatform
|
|
141
|
+
};
|