myaidev-method 0.2.23 → 0.2.24-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/.claude-plugin/plugin.json +251 -0
- package/PLUGIN_ARCHITECTURE.md +276 -0
- package/README.md +204 -0
- package/USER_GUIDE.md +436 -9
- package/bin/cli.js +370 -38
- package/dist/server/.tsbuildinfo +1 -1
- package/extension.json +174 -0
- package/hooks/hooks.json +221 -0
- package/marketplace.json +179 -0
- package/package.json +24 -7
- package/skills/content-verifier/SKILL.md +178 -0
- package/skills/content-writer/SKILL.md +151 -0
- package/skills/coolify-deployer/SKILL.md +207 -0
- package/skills/openstack-manager/SKILL.md +213 -0
- package/skills/security-auditor/SKILL.md +180 -0
- package/skills/security-tester/SKILL.md +171 -0
- package/skills/sparc-architect/SKILL.md +146 -0
- package/skills/sparc-coder/SKILL.md +136 -0
- package/skills/sparc-documenter/SKILL.md +195 -0
- package/skills/sparc-reviewer/SKILL.md +179 -0
- package/skills/sparc-tester/SKILL.md +156 -0
- package/skills/visual-generator/SKILL.md +147 -0
- package/skills/wordpress-publisher/SKILL.md +150 -0
- package/src/config/workflows.js +28 -44
- package/src/lib/ascii-banner.js +214 -0
- package/src/lib/config-manager.js +470 -0
- package/src/lib/content-coordinator.js +2562 -0
- package/src/lib/content-generator.js +427 -0
- package/src/lib/html-conversion-utils.js +843 -0
- package/src/lib/installation-detector.js +266 -0
- package/src/lib/seo-optimizer.js +515 -0
- package/src/lib/visual-config-utils.js +1 -1
- package/src/lib/visual-generation-utils.js +34 -14
- package/src/lib/wordpress-client.js +633 -0
- package/src/lib/workflow-installer.js +3 -3
- package/src/scripts/generate-visual-cli.js +39 -10
- package/src/scripts/html-conversion-cli.js +526 -0
- package/src/scripts/init/configure.js +436 -0
- package/src/scripts/init/install.js +460 -0
- package/src/scripts/ping.js +0 -1
- package/src/scripts/utils/file-utils.js +404 -0
- package/src/scripts/utils/logger.js +300 -0
- package/src/scripts/utils/write-content.js +293 -0
- package/src/templates/claude/agents/content-production-coordinator.md +689 -15
- package/src/templates/claude/agents/visual-content-generator.md +129 -4
- package/src/templates/claude/commands/myai-content-enrichment.md +227 -0
- package/src/templates/claude/commands/myai-content-writer.md +48 -37
- package/src/templates/claude/commands/myai-convert-html.md +186 -0
- package/src/templates/claude/commands/myai-coordinate-content.md +347 -11
- package/src/templates/diagrams/architecture.d2 +52 -0
- package/src/templates/diagrams/flowchart.d2 +42 -0
- package/src/templates/diagrams/sequence.d2 +47 -0
- package/src/templates/docs/content-creation-guide.md +164 -0
- package/src/templates/docs/deployment-guide.md +336 -0
- package/src/templates/docs/visual-generation-guide.md +248 -0
- package/src/templates/docs/wordpress-publishing-guide.md +208 -0
- package/src/templates/infographics/comparison-table.html +347 -0
- package/src/templates/infographics/data-chart.html +268 -0
- package/src/templates/infographics/process-flow.html +365 -0
- /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;
|