myaidev-method 0.2.23 → 0.2.24-1

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 (30) hide show
  1. package/bin/cli.js +218 -38
  2. package/dist/server/.tsbuildinfo +1 -1
  3. package/package.json +10 -5
  4. package/src/config/workflows.js +28 -44
  5. package/src/lib/ascii-banner.js +214 -0
  6. package/src/lib/config-manager.js +470 -0
  7. package/src/lib/content-generator.js +427 -0
  8. package/src/lib/html-conversion-utils.js +843 -0
  9. package/src/lib/seo-optimizer.js +515 -0
  10. package/src/lib/wordpress-client.js +633 -0
  11. package/src/lib/workflow-installer.js +3 -3
  12. package/src/scripts/html-conversion-cli.js +526 -0
  13. package/src/scripts/init/configure.js +436 -0
  14. package/src/scripts/init/install.js +460 -0
  15. package/src/scripts/utils/file-utils.js +404 -0
  16. package/src/scripts/utils/logger.js +300 -0
  17. package/src/scripts/utils/write-content.js +293 -0
  18. package/src/templates/claude/agents/visual-content-generator.md +129 -4
  19. package/src/templates/claude/commands/myai-convert-html.md +186 -0
  20. package/src/templates/diagrams/architecture.d2 +52 -0
  21. package/src/templates/diagrams/flowchart.d2 +42 -0
  22. package/src/templates/diagrams/sequence.d2 +47 -0
  23. package/src/templates/docs/content-creation-guide.md +164 -0
  24. package/src/templates/docs/deployment-guide.md +336 -0
  25. package/src/templates/docs/visual-generation-guide.md +248 -0
  26. package/src/templates/docs/wordpress-publishing-guide.md +208 -0
  27. package/src/templates/infographics/comparison-table.html +347 -0
  28. package/src/templates/infographics/data-chart.html +268 -0
  29. package/src/templates/infographics/process-flow.html +365 -0
  30. /package/src/scripts/{wordpress-health-check.js → wordpress/wordpress-health-check.js} +0 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * File Utilities
3
+ * Common file system operations for MyAIDev Method CLI
4
+ */
5
+
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { logger } from './logger.js';
9
+
10
+ /**
11
+ * Read and parse a configuration file (JSON or YAML-like)
12
+ * @param {string} filePath - Path to config file
13
+ * @returns {Promise<Object|null>} Parsed config or null if not found
14
+ */
15
+ export async function readConfigFile(filePath) {
16
+ try {
17
+ const exists = await fs.pathExists(filePath);
18
+ if (!exists) {
19
+ logger.debug(`Config file not found: ${filePath}`);
20
+ return null;
21
+ }
22
+
23
+ const content = await fs.readFile(filePath, 'utf-8');
24
+ const ext = path.extname(filePath).toLowerCase();
25
+
26
+ if (ext === '.json') {
27
+ return JSON.parse(content);
28
+ }
29
+
30
+ // For simple key=value or YAML-like files
31
+ if (ext === '.env' || ext === '') {
32
+ return parseEnvContent(content);
33
+ }
34
+
35
+ // Try JSON parse for unknown extensions
36
+ try {
37
+ return JSON.parse(content);
38
+ } catch {
39
+ return { raw: content };
40
+ }
41
+ } catch (error) {
42
+ logger.error(`Failed to read config file ${filePath}: ${error.message}`);
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Write a configuration file
49
+ * @param {string} filePath - Path to write to
50
+ * @param {Object|string} content - Content to write
51
+ * @param {Object} [options] - Write options
52
+ * @param {boolean} [options.pretty=true] - Pretty print JSON
53
+ * @param {boolean} [options.backup=false] - Create backup before overwriting
54
+ */
55
+ export async function writeConfigFile(filePath, content, options = {}) {
56
+ const { pretty = true, backup = false } = options;
57
+
58
+ try {
59
+ // Create backup if requested and file exists
60
+ if (backup && await fs.pathExists(filePath)) {
61
+ await backupFile(filePath);
62
+ }
63
+
64
+ // Ensure directory exists
65
+ await fs.ensureDir(path.dirname(filePath));
66
+
67
+ const ext = path.extname(filePath).toLowerCase();
68
+ let output;
69
+
70
+ if (ext === '.json') {
71
+ output = pretty ? JSON.stringify(content, null, 2) : JSON.stringify(content);
72
+ } else if (ext === '.env' || filePath.includes('.env')) {
73
+ output = formatEnvContent(content);
74
+ } else if (typeof content === 'string') {
75
+ output = content;
76
+ } else {
77
+ output = pretty ? JSON.stringify(content, null, 2) : JSON.stringify(content);
78
+ }
79
+
80
+ await fs.writeFile(filePath, output, 'utf-8');
81
+ logger.debug(`Wrote config file: ${filePath}`);
82
+ return true;
83
+ } catch (error) {
84
+ logger.error(`Failed to write config file ${filePath}: ${error.message}`);
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Create a backup of a file
91
+ * @param {string} filePath - File to backup
92
+ * @param {string} [backupDir] - Optional backup directory (defaults to same directory)
93
+ * @returns {Promise<string|null>} Backup file path or null on failure
94
+ */
95
+ export async function backupFile(filePath, backupDir) {
96
+ try {
97
+ const exists = await fs.pathExists(filePath);
98
+ if (!exists) {
99
+ logger.warn(`Cannot backup non-existent file: ${filePath}`);
100
+ return null;
101
+ }
102
+
103
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
104
+ const basename = path.basename(filePath);
105
+ const dirname = backupDir || path.dirname(filePath);
106
+
107
+ await fs.ensureDir(dirname);
108
+
109
+ const backupPath = path.join(dirname, `${basename}.backup-${timestamp}`);
110
+ await fs.copy(filePath, backupPath);
111
+
112
+ logger.debug(`Created backup: ${backupPath}`);
113
+ return backupPath;
114
+ } catch (error) {
115
+ logger.error(`Failed to backup file ${filePath}: ${error.message}`);
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Ensure a directory exists, creating it if necessary
122
+ * @param {string} dirPath - Directory path
123
+ * @returns {Promise<boolean>} True if successful
124
+ */
125
+ export async function ensureDir(dirPath) {
126
+ try {
127
+ await fs.ensureDir(dirPath);
128
+ return true;
129
+ } catch (error) {
130
+ logger.error(`Failed to create directory ${dirPath}: ${error.message}`);
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Copy a file tree from source to destination
137
+ * @param {string} source - Source directory
138
+ * @param {string} dest - Destination directory
139
+ * @param {Object} [options] - Copy options
140
+ * @param {string[]} [options.exclude] - Patterns to exclude
141
+ * @param {boolean} [options.overwrite=true] - Overwrite existing files
142
+ */
143
+ export async function copyFileTree(source, dest, options = {}) {
144
+ const { exclude = [], overwrite = true } = options;
145
+
146
+ try {
147
+ await fs.copy(source, dest, {
148
+ overwrite,
149
+ filter: (src) => {
150
+ const relativePath = path.relative(source, src);
151
+ return !exclude.some(pattern => {
152
+ if (pattern.includes('*')) {
153
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
154
+ return regex.test(relativePath);
155
+ }
156
+ return relativePath.includes(pattern);
157
+ });
158
+ }
159
+ });
160
+ logger.debug(`Copied ${source} to ${dest}`);
161
+ return true;
162
+ } catch (error) {
163
+ logger.error(`Failed to copy file tree: ${error.message}`);
164
+ return false;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Read an .env file
170
+ * @param {string} envPath - Path to .env file
171
+ * @returns {Promise<Object>} Environment variables as object
172
+ */
173
+ export async function readEnvFile(envPath) {
174
+ try {
175
+ const exists = await fs.pathExists(envPath);
176
+ if (!exists) {
177
+ return {};
178
+ }
179
+
180
+ const content = await fs.readFile(envPath, 'utf-8');
181
+ return parseEnvContent(content);
182
+ } catch (error) {
183
+ logger.error(`Failed to read .env file ${envPath}: ${error.message}`);
184
+ return {};
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Write an .env file
190
+ * @param {string} envPath - Path to .env file
191
+ * @param {Object} config - Environment variables to write
192
+ * @param {boolean} [merge=true] - Merge with existing values
193
+ */
194
+ export async function writeEnvFile(envPath, config, merge = true) {
195
+ try {
196
+ let existing = {};
197
+ if (merge) {
198
+ existing = await readEnvFile(envPath);
199
+ }
200
+
201
+ const merged = { ...existing, ...config };
202
+ const content = formatEnvContent(merged);
203
+
204
+ await fs.ensureDir(path.dirname(envPath));
205
+ await fs.writeFile(envPath, content, 'utf-8');
206
+
207
+ logger.debug(`Wrote .env file: ${envPath}`);
208
+ return true;
209
+ } catch (error) {
210
+ logger.error(`Failed to write .env file ${envPath}: ${error.message}`);
211
+ return false;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Parse .env file content into object
217
+ * @param {string} content - File content
218
+ * @returns {Object} Parsed environment variables
219
+ */
220
+ function parseEnvContent(content) {
221
+ const result = {};
222
+ const lines = content.split('\n');
223
+
224
+ for (const line of lines) {
225
+ const trimmed = line.trim();
226
+
227
+ // Skip empty lines and comments
228
+ if (!trimmed || trimmed.startsWith('#')) {
229
+ continue;
230
+ }
231
+
232
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
233
+ if (match) {
234
+ const key = match[1].trim();
235
+ let value = match[2].trim();
236
+
237
+ // Remove quotes if present
238
+ if ((value.startsWith('"') && value.endsWith('"')) ||
239
+ (value.startsWith("'") && value.endsWith("'"))) {
240
+ value = value.slice(1, -1);
241
+ }
242
+
243
+ result[key] = value;
244
+ }
245
+ }
246
+
247
+ return result;
248
+ }
249
+
250
+ /**
251
+ * Format object as .env file content
252
+ * @param {Object} config - Configuration object
253
+ * @returns {string} Formatted .env content
254
+ */
255
+ function formatEnvContent(config) {
256
+ const lines = [];
257
+
258
+ for (const [key, value] of Object.entries(config)) {
259
+ if (value === undefined || value === null) {
260
+ continue;
261
+ }
262
+
263
+ // Quote values that contain spaces or special characters
264
+ const stringValue = String(value);
265
+ const needsQuotes = stringValue.includes(' ') ||
266
+ stringValue.includes('#') ||
267
+ stringValue.includes('=');
268
+
269
+ if (needsQuotes) {
270
+ lines.push(`${key}="${stringValue}"`);
271
+ } else {
272
+ lines.push(`${key}=${stringValue}`);
273
+ }
274
+ }
275
+
276
+ return lines.join('\n') + '\n';
277
+ }
278
+
279
+ /**
280
+ * Check if a path is a file
281
+ * @param {string} filePath - Path to check
282
+ * @returns {Promise<boolean>}
283
+ */
284
+ export async function isFile(filePath) {
285
+ try {
286
+ const stat = await fs.stat(filePath);
287
+ return stat.isFile();
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Check if a path is a directory
295
+ * @param {string} dirPath - Path to check
296
+ * @returns {Promise<boolean>}
297
+ */
298
+ export async function isDirectory(dirPath) {
299
+ try {
300
+ const stat = await fs.stat(dirPath);
301
+ return stat.isDirectory();
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Find files matching a pattern in a directory
309
+ * @param {string} dirPath - Directory to search
310
+ * @param {string|RegExp} pattern - Pattern to match
311
+ * @param {Object} [options] - Search options
312
+ * @param {boolean} [options.recursive=true] - Search recursively
313
+ * @returns {Promise<string[]>} Array of matching file paths
314
+ */
315
+ export async function findFiles(dirPath, pattern, options = {}) {
316
+ const { recursive = true } = options;
317
+ const results = [];
318
+
319
+ try {
320
+ const exists = await fs.pathExists(dirPath);
321
+ if (!exists) {
322
+ return results;
323
+ }
324
+
325
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
326
+
327
+ for (const entry of entries) {
328
+ const fullPath = path.join(dirPath, entry.name);
329
+
330
+ if (entry.isFile()) {
331
+ const matches = typeof pattern === 'string'
332
+ ? entry.name.includes(pattern) || entry.name.endsWith(pattern)
333
+ : pattern.test(entry.name);
334
+
335
+ if (matches) {
336
+ results.push(fullPath);
337
+ }
338
+ } else if (entry.isDirectory() && recursive) {
339
+ const subResults = await findFiles(fullPath, pattern, options);
340
+ results.push(...subResults);
341
+ }
342
+ }
343
+
344
+ return results;
345
+ } catch (error) {
346
+ logger.error(`Failed to find files in ${dirPath}: ${error.message}`);
347
+ return results;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Get the project root directory (where package.json is)
353
+ * @param {string} [startDir] - Starting directory
354
+ * @returns {Promise<string|null>} Project root or null
355
+ */
356
+ export async function findProjectRoot(startDir = process.cwd()) {
357
+ let currentDir = startDir;
358
+
359
+ while (currentDir !== path.dirname(currentDir)) {
360
+ const packageJsonPath = path.join(currentDir, 'package.json');
361
+ if (await fs.pathExists(packageJsonPath)) {
362
+ return currentDir;
363
+ }
364
+ currentDir = path.dirname(currentDir);
365
+ }
366
+
367
+ return null;
368
+ }
369
+
370
+ /**
371
+ * Safely remove a file or directory
372
+ * @param {string} targetPath - Path to remove
373
+ * @returns {Promise<boolean>} True if successful
374
+ */
375
+ export async function safeRemove(targetPath) {
376
+ try {
377
+ const exists = await fs.pathExists(targetPath);
378
+ if (!exists) {
379
+ return true;
380
+ }
381
+
382
+ await fs.remove(targetPath);
383
+ logger.debug(`Removed: ${targetPath}`);
384
+ return true;
385
+ } catch (error) {
386
+ logger.error(`Failed to remove ${targetPath}: ${error.message}`);
387
+ return false;
388
+ }
389
+ }
390
+
391
+ export default {
392
+ readConfigFile,
393
+ writeConfigFile,
394
+ backupFile,
395
+ ensureDir,
396
+ copyFileTree,
397
+ readEnvFile,
398
+ writeEnvFile,
399
+ isFile,
400
+ isDirectory,
401
+ findFiles,
402
+ findProjectRoot,
403
+ safeRemove
404
+ };
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Logger Utility
3
+ * Provides consistent logging, formatting, and progress tracking
4
+ * Used across all MyAIDev Method CLI scripts
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+
10
+ // Log levels
11
+ const LOG_LEVELS = {
12
+ DEBUG: 0,
13
+ INFO: 1,
14
+ WARN: 2,
15
+ ERROR: 3,
16
+ SILENT: 4
17
+ };
18
+
19
+ // Current log level (can be set via environment variable)
20
+ let currentLogLevel = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase()] ?? LOG_LEVELS.INFO;
21
+
22
+ /**
23
+ * Set the log level
24
+ * @param {string} level - Log level (DEBUG, INFO, WARN, ERROR, SILENT)
25
+ */
26
+ export function setLogLevel(level) {
27
+ const upperLevel = level.toUpperCase();
28
+ if (LOG_LEVELS[upperLevel] !== undefined) {
29
+ currentLogLevel = LOG_LEVELS[upperLevel];
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Logger object with methods for different log levels
35
+ */
36
+ export const logger = {
37
+ /**
38
+ * Log debug message (only shown when LOG_LEVEL=DEBUG)
39
+ * @param {string} message - Debug message
40
+ * @param {...any} args - Additional arguments
41
+ */
42
+ debug(message, ...args) {
43
+ if (currentLogLevel <= LOG_LEVELS.DEBUG) {
44
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
45
+ }
46
+ },
47
+
48
+ /**
49
+ * Log info message
50
+ * @param {string} message - Info message
51
+ * @param {...any} args - Additional arguments
52
+ */
53
+ info(message, ...args) {
54
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
55
+ console.log(chalk.blue('ℹ'), message, ...args);
56
+ }
57
+ },
58
+
59
+ /**
60
+ * Log success message
61
+ * @param {string} message - Success message
62
+ * @param {...any} args - Additional arguments
63
+ */
64
+ success(message, ...args) {
65
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
66
+ console.log(chalk.green('✓'), message, ...args);
67
+ }
68
+ },
69
+
70
+ /**
71
+ * Log warning message
72
+ * @param {string} message - Warning message
73
+ * @param {...any} args - Additional arguments
74
+ */
75
+ warn(message, ...args) {
76
+ if (currentLogLevel <= LOG_LEVELS.WARN) {
77
+ console.log(chalk.yellow('⚠'), chalk.yellow(message), ...args);
78
+ }
79
+ },
80
+
81
+ /**
82
+ * Log error message
83
+ * @param {string} message - Error message
84
+ * @param {...any} args - Additional arguments
85
+ */
86
+ error(message, ...args) {
87
+ if (currentLogLevel <= LOG_LEVELS.ERROR) {
88
+ console.error(chalk.red('✗'), chalk.red(message), ...args);
89
+ }
90
+ },
91
+
92
+ /**
93
+ * Log a plain message without prefix
94
+ * @param {string} message - Message
95
+ * @param {...any} args - Additional arguments
96
+ */
97
+ log(message, ...args) {
98
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
99
+ console.log(message, ...args);
100
+ }
101
+ },
102
+
103
+ /**
104
+ * Log a blank line
105
+ */
106
+ blank() {
107
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
108
+ console.log();
109
+ }
110
+ },
111
+
112
+ /**
113
+ * Log a header/title
114
+ * @param {string} title - Header text
115
+ */
116
+ header(title) {
117
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
118
+ console.log();
119
+ console.log(chalk.bold.cyan(`═══ ${title} ═══`));
120
+ console.log();
121
+ }
122
+ },
123
+
124
+ /**
125
+ * Log a section divider
126
+ * @param {string} title - Section title
127
+ */
128
+ section(title) {
129
+ if (currentLogLevel <= LOG_LEVELS.INFO) {
130
+ console.log();
131
+ console.log(chalk.bold(`── ${title} ──`));
132
+ }
133
+ }
134
+ };
135
+
136
+ /**
137
+ * Format data as a table
138
+ * @param {Array<Object>} data - Array of objects to display
139
+ * @param {Array<{key: string, header: string, width?: number}>} columns - Column definitions
140
+ * @returns {string} Formatted table string
141
+ */
142
+ export function formatTable(data, columns) {
143
+ if (!data || data.length === 0) {
144
+ return '';
145
+ }
146
+
147
+ // Calculate column widths
148
+ const widths = columns.map(col => {
149
+ if (col.width) return col.width;
150
+ const headerLen = col.header.length;
151
+ const maxDataLen = Math.max(...data.map(row => String(row[col.key] ?? '').length));
152
+ return Math.max(headerLen, maxDataLen);
153
+ });
154
+
155
+ // Build header row
156
+ const headerRow = columns.map((col, i) =>
157
+ chalk.bold(col.header.padEnd(widths[i]))
158
+ ).join(' │ ');
159
+
160
+ // Build separator
161
+ const separator = widths.map(w => '─'.repeat(w)).join('─┼─');
162
+
163
+ // Build data rows
164
+ const dataRows = data.map(row =>
165
+ columns.map((col, i) =>
166
+ String(row[col.key] ?? '').padEnd(widths[i])
167
+ ).join(' │ ')
168
+ );
169
+
170
+ return [headerRow, separator, ...dataRows].join('\n');
171
+ }
172
+
173
+ /**
174
+ * Format items as a list
175
+ * @param {Array<string>} items - Items to list
176
+ * @param {string} [prefix='•'] - List item prefix
177
+ * @returns {string} Formatted list string
178
+ */
179
+ export function formatList(items, prefix = '•') {
180
+ return items.map(item => ` ${prefix} ${item}`).join('\n');
181
+ }
182
+
183
+ /**
184
+ * Progress tracker for multi-step operations
185
+ */
186
+ export class ProgressTracker {
187
+ constructor(total, label = 'Progress') {
188
+ this.total = total;
189
+ this.current = 0;
190
+ this.label = label;
191
+ this.failures = [];
192
+ this.startTime = Date.now();
193
+ }
194
+
195
+ /**
196
+ * Start the progress tracker
197
+ */
198
+ start() {
199
+ logger.info(`${this.label}: Starting (${this.total} items)`);
200
+ }
201
+
202
+ /**
203
+ * Increment progress
204
+ * @param {string} [item] - Current item being processed
205
+ */
206
+ increment(item) {
207
+ this.current++;
208
+ if (item && currentLogLevel <= LOG_LEVELS.DEBUG) {
209
+ logger.debug(`[${this.current}/${this.total}] ${item}`);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Record a failure
215
+ * @param {string} item - Item that failed
216
+ * @param {Error|string} error - Error information
217
+ */
218
+ fail(item, error) {
219
+ this.failures.push({ item, error: error?.message || error });
220
+ logger.warn(`Failed: ${item}`);
221
+ }
222
+
223
+ /**
224
+ * Complete the progress tracker
225
+ * @returns {{success: number, failed: number, duration: number}}
226
+ */
227
+ complete() {
228
+ const duration = Date.now() - this.startTime;
229
+ const success = this.current - this.failures.length;
230
+
231
+ logger.blank();
232
+ if (this.failures.length === 0) {
233
+ logger.success(`${this.label}: Completed ${this.current}/${this.total} items (${duration}ms)`);
234
+ } else {
235
+ logger.warn(`${this.label}: ${success} succeeded, ${this.failures.length} failed (${duration}ms)`);
236
+ this.failures.forEach(f => logger.error(` - ${f.item}: ${f.error}`));
237
+ }
238
+
239
+ return { success, failed: this.failures.length, duration };
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Create a spinner for async operations
245
+ * @param {string} message - Spinner message
246
+ * @returns {ora.Ora} Ora spinner instance
247
+ */
248
+ export function createSpinner(message) {
249
+ return ora({
250
+ text: message,
251
+ spinner: 'dots',
252
+ color: 'cyan'
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Wrap an async function with a spinner
258
+ * @param {string} message - Spinner message
259
+ * @param {Function} fn - Async function to execute
260
+ * @returns {Promise<any>} Result of the function
261
+ */
262
+ export async function withSpinner(message, fn) {
263
+ const spinner = createSpinner(message);
264
+ spinner.start();
265
+
266
+ try {
267
+ const result = await fn();
268
+ spinner.succeed();
269
+ return result;
270
+ } catch (error) {
271
+ spinner.fail();
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Format bytes to human-readable string
278
+ * @param {number} bytes - Number of bytes
279
+ * @returns {string} Human-readable string (e.g., "1.5 MB")
280
+ */
281
+ export function formatBytes(bytes) {
282
+ if (bytes === 0) return '0 B';
283
+ const k = 1024;
284
+ const sizes = ['B', 'KB', 'MB', 'GB'];
285
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
286
+ return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
287
+ }
288
+
289
+ /**
290
+ * Format duration to human-readable string
291
+ * @param {number} ms - Duration in milliseconds
292
+ * @returns {string} Human-readable string (e.g., "2.5s")
293
+ */
294
+ export function formatDuration(ms) {
295
+ if (ms < 1000) return `${ms}ms`;
296
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
297
+ return `${(ms / 60000).toFixed(1)}m`;
298
+ }
299
+
300
+ export default logger;