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.
@@ -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
+ };