myaidev-method 0.2.12 ā 0.2.15
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/CLAUDE.md +46 -0
- package/.claude/agents/content-production-coordinator.md +111 -0
- package/.claude/agents/proprietary-content-verifier.md +96 -0
- package/.claude/agents/visual-content-generator.md +520 -0
- package/.claude/commands/myai-coordinate-content.md +136 -0
- package/.claude/settings.local.json +3 -2
- package/.env.example +33 -0
- package/CHANGELOG.md +64 -0
- package/CONTENT_CREATION_GUIDE.md +3399 -0
- package/DEVELOPER_USE_CASES.md +2085 -0
- package/README.md +209 -2
- package/VISUAL_GENERATION_FILE_ORGANIZATION.md +105 -0
- package/bin/cli.js +46 -0
- package/package.json +17 -2
- package/src/lib/asset-management.js +532 -0
- package/src/lib/visual-config-utils.js +424 -0
- package/src/lib/visual-generation-utils.js +668 -0
- package/src/scripts/configure-visual-apis.js +413 -0
- package/src/scripts/generate-visual-cli.js +279 -0
- package/src/templates/claude/agents/content-production-coordinator.md +111 -0
- package/src/templates/claude/agents/content-writer.md +209 -4
- package/src/templates/claude/agents/proprietary-content-verifier.md +96 -0
- package/src/templates/claude/agents/visual-content-generator.md +520 -0
- package/src/templates/claude/commands/myai-content-writer.md +33 -8
- package/src/templates/claude/commands/myai-coordinate-content.md +136 -0
- package/src/templates/claude/commands/myai-generate-visual.md +318 -0
- package/src/templates/codex/commands/myai-generate-visual.md +307 -0
- package/src/templates/gemini/commands/myai-generate-visual.md +200 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Management Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles file storage, organization, and tracking for generated visual content.
|
|
5
|
+
* Supports images and videos with automatic directory organization by date.
|
|
6
|
+
*
|
|
7
|
+
* Platform support: Claude Code, Gemini CLI, Codex CLI
|
|
8
|
+
*
|
|
9
|
+
* @module asset-management
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import dotenv from 'dotenv';
|
|
15
|
+
|
|
16
|
+
dotenv.config();
|
|
17
|
+
|
|
18
|
+
// Default asset storage path
|
|
19
|
+
const DEFAULT_ASSETS_PATH = './content-assets';
|
|
20
|
+
|
|
21
|
+
// Usage tracking file
|
|
22
|
+
const USAGE_STATS_FILE = '.visual-usage-stats.json';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get configured assets path from environment or use default
|
|
26
|
+
*
|
|
27
|
+
* @returns {string} Assets directory path
|
|
28
|
+
*/
|
|
29
|
+
export function getAssetsPath() {
|
|
30
|
+
return process.env.VISUAL_ASSETS_PATH || DEFAULT_ASSETS_PATH;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create asset directory structure for a specific date
|
|
35
|
+
* Creates: content-assets/images/YYYY-MM-DD/ and content-assets/videos/YYYY-MM-DD/
|
|
36
|
+
*
|
|
37
|
+
* @param {Date} date - Date for directory organization (defaults to today)
|
|
38
|
+
* @returns {Promise<Object>} Created directory paths
|
|
39
|
+
*/
|
|
40
|
+
export async function createAssetDirectory(date = new Date()) {
|
|
41
|
+
const assetsPath = getAssetsPath();
|
|
42
|
+
const dateStr = formatDate(date);
|
|
43
|
+
|
|
44
|
+
const imagePath = path.join(assetsPath, 'images', dateStr);
|
|
45
|
+
const videoPath = path.join(assetsPath, 'videos', dateStr);
|
|
46
|
+
|
|
47
|
+
await fs.ensureDir(imagePath);
|
|
48
|
+
await fs.ensureDir(videoPath);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
images: imagePath,
|
|
52
|
+
videos: videoPath,
|
|
53
|
+
date: dateStr
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate filename for asset based on type and description
|
|
59
|
+
*
|
|
60
|
+
* @param {string} type - Asset type (hero, illustration, diagram, screenshot, video)
|
|
61
|
+
* @param {string} slug - Descriptive slug (will be sanitized)
|
|
62
|
+
* @param {string} extension - File extension (png, jpg, mp4, etc.)
|
|
63
|
+
* @returns {string} Generated filename
|
|
64
|
+
*/
|
|
65
|
+
export function generateFilename(type, slug, extension = 'png') {
|
|
66
|
+
// Sanitize slug: lowercase, replace spaces/special chars with hyphens
|
|
67
|
+
const sanitized = slug
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
70
|
+
.replace(/^-+|-+$/g, '')
|
|
71
|
+
.substring(0, 50); // Max 50 chars
|
|
72
|
+
|
|
73
|
+
const timestamp = Date.now().toString().slice(-6); // Last 6 digits for uniqueness
|
|
74
|
+
|
|
75
|
+
return `${type}-${sanitized}-${timestamp}.${extension}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Save image to content-assets directory
|
|
80
|
+
*
|
|
81
|
+
* @param {Buffer} buffer - Image data buffer
|
|
82
|
+
* @param {Object} metadata - Image metadata
|
|
83
|
+
* @param {string} metadata.type - Image type (hero, illustration, diagram, screenshot)
|
|
84
|
+
* @param {string} metadata.description - Image description for filename
|
|
85
|
+
* @param {string} metadata.service - Service used (gemini, imagen, dalle)
|
|
86
|
+
* @param {number} metadata.cost - Generation cost
|
|
87
|
+
* @param {string} metadata.prompt - Original prompt
|
|
88
|
+
* @param {Date} metadata.date - Creation date (defaults to now)
|
|
89
|
+
* @returns {Promise<Object>} Saved file information
|
|
90
|
+
*/
|
|
91
|
+
export async function saveImage(buffer, metadata) {
|
|
92
|
+
const {
|
|
93
|
+
type = 'general',
|
|
94
|
+
description,
|
|
95
|
+
service,
|
|
96
|
+
cost = 0,
|
|
97
|
+
prompt,
|
|
98
|
+
date = new Date()
|
|
99
|
+
} = metadata;
|
|
100
|
+
|
|
101
|
+
// Create directory structure
|
|
102
|
+
const dirs = await createAssetDirectory(date);
|
|
103
|
+
|
|
104
|
+
// Generate filename
|
|
105
|
+
const filename = generateFilename(type, description, 'png');
|
|
106
|
+
const filepath = path.join(dirs.images, filename);
|
|
107
|
+
|
|
108
|
+
// Save image
|
|
109
|
+
await fs.writeFile(filepath, buffer);
|
|
110
|
+
|
|
111
|
+
// Track usage
|
|
112
|
+
await trackCost(service, cost, {
|
|
113
|
+
type: 'image',
|
|
114
|
+
filename,
|
|
115
|
+
prompt,
|
|
116
|
+
date
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const relativePath = path.relative(process.cwd(), filepath);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
filepath,
|
|
123
|
+
relativePath,
|
|
124
|
+
filename,
|
|
125
|
+
type,
|
|
126
|
+
service,
|
|
127
|
+
cost,
|
|
128
|
+
size: buffer.length,
|
|
129
|
+
date: date.toISOString()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Save video to content-assets directory
|
|
135
|
+
*
|
|
136
|
+
* @param {Buffer} buffer - Video data buffer
|
|
137
|
+
* @param {Object} metadata - Video metadata
|
|
138
|
+
* @param {string} metadata.description - Video description for filename
|
|
139
|
+
* @param {string} metadata.service - Service used (veo)
|
|
140
|
+
* @param {number} metadata.cost - Generation cost
|
|
141
|
+
* @param {string} metadata.prompt - Original prompt
|
|
142
|
+
* @param {number} metadata.duration - Video duration in seconds
|
|
143
|
+
* @param {Date} metadata.date - Creation date (defaults to now)
|
|
144
|
+
* @returns {Promise<Object>} Saved file information
|
|
145
|
+
*/
|
|
146
|
+
export async function saveVideo(buffer, metadata) {
|
|
147
|
+
const {
|
|
148
|
+
description,
|
|
149
|
+
service,
|
|
150
|
+
cost = 0,
|
|
151
|
+
prompt,
|
|
152
|
+
duration = 0,
|
|
153
|
+
date = new Date()
|
|
154
|
+
} = metadata;
|
|
155
|
+
|
|
156
|
+
// Create directory structure
|
|
157
|
+
const dirs = await createAssetDirectory(date);
|
|
158
|
+
|
|
159
|
+
// Generate filename
|
|
160
|
+
const filename = generateFilename('video', description, 'mp4');
|
|
161
|
+
const filepath = path.join(dirs.videos, filename);
|
|
162
|
+
|
|
163
|
+
// Save video
|
|
164
|
+
await fs.writeFile(filepath, buffer);
|
|
165
|
+
|
|
166
|
+
// Track usage
|
|
167
|
+
await trackCost(service, cost, {
|
|
168
|
+
type: 'video',
|
|
169
|
+
filename,
|
|
170
|
+
prompt,
|
|
171
|
+
duration,
|
|
172
|
+
date
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const relativePath = path.relative(process.cwd(), filepath);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
filepath,
|
|
179
|
+
relativePath,
|
|
180
|
+
filename,
|
|
181
|
+
service,
|
|
182
|
+
cost,
|
|
183
|
+
duration,
|
|
184
|
+
size: buffer.length,
|
|
185
|
+
date: date.toISOString()
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate markdown reference for image
|
|
191
|
+
*
|
|
192
|
+
* @param {string} filepath - Relative or absolute file path
|
|
193
|
+
* @param {string} altText - Alt text for accessibility
|
|
194
|
+
* @param {string} caption - Optional caption
|
|
195
|
+
* @returns {string} Markdown image reference
|
|
196
|
+
*/
|
|
197
|
+
export function generateMarkdownReference(filepath, altText, caption = null) {
|
|
198
|
+
// Convert to relative path if absolute
|
|
199
|
+
const relativePath = filepath.startsWith('./') || filepath.startsWith('../')
|
|
200
|
+
? filepath
|
|
201
|
+
: `./${filepath}`;
|
|
202
|
+
|
|
203
|
+
let markdown = ``;
|
|
204
|
+
|
|
205
|
+
if (caption) {
|
|
206
|
+
markdown += `\n*${caption}*`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return markdown;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Track cost and usage statistics
|
|
214
|
+
*
|
|
215
|
+
* @param {string} service - Service name
|
|
216
|
+
* @param {number} cost - Cost in USD
|
|
217
|
+
* @param {Object} metadata - Additional metadata to track
|
|
218
|
+
* @returns {Promise<void>}
|
|
219
|
+
*/
|
|
220
|
+
export async function trackCost(service, cost, metadata = {}) {
|
|
221
|
+
const statsPath = path.join(process.cwd(), USAGE_STATS_FILE);
|
|
222
|
+
|
|
223
|
+
// Load existing stats
|
|
224
|
+
let stats = {
|
|
225
|
+
totalCost: 0,
|
|
226
|
+
daily: {},
|
|
227
|
+
monthly: {},
|
|
228
|
+
byService: {},
|
|
229
|
+
history: []
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (await fs.pathExists(statsPath)) {
|
|
233
|
+
stats = await fs.readJSON(statsPath);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Get date keys
|
|
237
|
+
const now = new Date();
|
|
238
|
+
const dateKey = formatDate(now);
|
|
239
|
+
const monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
240
|
+
|
|
241
|
+
// Update totals
|
|
242
|
+
stats.totalCost += cost;
|
|
243
|
+
|
|
244
|
+
// Update daily
|
|
245
|
+
stats.daily[dateKey] = (stats.daily[dateKey] || 0) + cost;
|
|
246
|
+
|
|
247
|
+
// Update monthly
|
|
248
|
+
stats.monthly[monthKey] = (stats.monthly[monthKey] || 0) + cost;
|
|
249
|
+
|
|
250
|
+
// Update by service
|
|
251
|
+
if (!stats.byService[service]) {
|
|
252
|
+
stats.byService[service] = { count: 0, cost: 0 };
|
|
253
|
+
}
|
|
254
|
+
stats.byService[service].count += 1;
|
|
255
|
+
stats.byService[service].cost += cost;
|
|
256
|
+
|
|
257
|
+
// Add to history
|
|
258
|
+
stats.history.push({
|
|
259
|
+
service,
|
|
260
|
+
cost,
|
|
261
|
+
date: now.toISOString(),
|
|
262
|
+
...metadata
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Keep only last 1000 history entries
|
|
266
|
+
if (stats.history.length > 1000) {
|
|
267
|
+
stats.history = stats.history.slice(-1000);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Save stats
|
|
271
|
+
await fs.writeJSON(statsPath, stats, { spaces: 2 });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get usage statistics
|
|
276
|
+
*
|
|
277
|
+
* @returns {Promise<Object>} Usage statistics
|
|
278
|
+
*/
|
|
279
|
+
export async function getUsageStats() {
|
|
280
|
+
const statsPath = path.join(process.cwd(), USAGE_STATS_FILE);
|
|
281
|
+
|
|
282
|
+
if (!await fs.pathExists(statsPath)) {
|
|
283
|
+
return {
|
|
284
|
+
totalCost: 0,
|
|
285
|
+
daily: {},
|
|
286
|
+
monthly: {},
|
|
287
|
+
byService: {},
|
|
288
|
+
history: []
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return await fs.readJSON(statsPath);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get today's usage cost
|
|
297
|
+
*
|
|
298
|
+
* @returns {Promise<number>} Today's cost in USD
|
|
299
|
+
*/
|
|
300
|
+
export async function getTodaysCost() {
|
|
301
|
+
const stats = await getUsageStats();
|
|
302
|
+
const today = formatDate(new Date());
|
|
303
|
+
return stats.daily[today] || 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get this month's usage cost
|
|
308
|
+
*
|
|
309
|
+
* @returns {Promise<number>} This month's cost in USD
|
|
310
|
+
*/
|
|
311
|
+
export async function getMonthCost() {
|
|
312
|
+
const stats = await getUsageStats();
|
|
313
|
+
const now = new Date();
|
|
314
|
+
const monthKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
315
|
+
return stats.monthly[monthKey] || 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Check if cost exceeds budget limits
|
|
320
|
+
*
|
|
321
|
+
* @param {number} estimatedCost - Estimated cost for next operation
|
|
322
|
+
* @returns {Promise<Object>} Budget check result
|
|
323
|
+
*/
|
|
324
|
+
export async function checkBudgetLimit(estimatedCost) {
|
|
325
|
+
const dailyBudget = parseFloat(process.env.VISUAL_DAILY_BUDGET || 5.0);
|
|
326
|
+
const monthlyBudget = parseFloat(process.env.VISUAL_MONTHLY_BUDGET || 50.0);
|
|
327
|
+
const warnThreshold = parseFloat(process.env.VISUAL_WARN_THRESHOLD || 0.8);
|
|
328
|
+
|
|
329
|
+
const todaysCost = await getTodaysCost();
|
|
330
|
+
const monthCost = await getMonthCost();
|
|
331
|
+
|
|
332
|
+
const newDailyCost = todaysCost + estimatedCost;
|
|
333
|
+
const newMonthlyCost = monthCost + estimatedCost;
|
|
334
|
+
|
|
335
|
+
const result = {
|
|
336
|
+
dailyBudget,
|
|
337
|
+
monthlyBudget,
|
|
338
|
+
todaysCost,
|
|
339
|
+
monthCost,
|
|
340
|
+
newDailyCost,
|
|
341
|
+
newMonthlyCost,
|
|
342
|
+
estimatedCost,
|
|
343
|
+
exceedsDailyBudget: newDailyCost > dailyBudget,
|
|
344
|
+
exceedsMonthlyBudget: newMonthlyCost > monthlyBudget,
|
|
345
|
+
dailyWarning: newDailyCost > (dailyBudget * warnThreshold),
|
|
346
|
+
monthlyWarning: newMonthlyCost > (monthlyBudget * warnThreshold),
|
|
347
|
+
canProceed: newDailyCost <= dailyBudget && newMonthlyCost <= monthlyBudget
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Generate usage report
|
|
355
|
+
*
|
|
356
|
+
* @param {string} period - Report period (today, week, month, all)
|
|
357
|
+
* @returns {Promise<string>} Formatted usage report
|
|
358
|
+
*/
|
|
359
|
+
export async function generateUsageReport(period = 'today') {
|
|
360
|
+
const stats = await getUsageStats();
|
|
361
|
+
const now = new Date();
|
|
362
|
+
|
|
363
|
+
let report = 'š Visual Content Generation Usage Report\n\n';
|
|
364
|
+
|
|
365
|
+
// Daily budget info
|
|
366
|
+
const dailyBudget = parseFloat(process.env.VISUAL_DAILY_BUDGET || 5.0);
|
|
367
|
+
const monthlyBudget = parseFloat(process.env.VISUAL_MONTHLY_BUDGET || 50.0);
|
|
368
|
+
const todaysCost = await getTodaysCost();
|
|
369
|
+
const monthCost = await getMonthCost();
|
|
370
|
+
|
|
371
|
+
report += `š° Budget Status:\n`;
|
|
372
|
+
report += ` Today: $${todaysCost.toFixed(2)} / $${dailyBudget.toFixed(2)} (${((todaysCost/dailyBudget)*100).toFixed(0)}%)\n`;
|
|
373
|
+
report += ` Month: $${monthCost.toFixed(2)} / $${monthlyBudget.toFixed(2)} (${((monthCost/monthlyBudget)*100).toFixed(0)}%)\n\n`;
|
|
374
|
+
|
|
375
|
+
// Service breakdown
|
|
376
|
+
report += `šØ By Service:\n`;
|
|
377
|
+
for (const [service, data] of Object.entries(stats.byService)) {
|
|
378
|
+
report += ` ${service}: ${data.count} generations, $${data.cost.toFixed(2)}\n`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
report += `\nšµ Total Lifetime Cost: $${stats.totalCost.toFixed(2)}\n`;
|
|
382
|
+
|
|
383
|
+
return report;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clean up old assets (optional maintenance)
|
|
388
|
+
*
|
|
389
|
+
* @param {number} daysOld - Delete assets older than this many days
|
|
390
|
+
* @returns {Promise<Object>} Cleanup results
|
|
391
|
+
*/
|
|
392
|
+
export async function cleanupOldAssets(daysOld = 30) {
|
|
393
|
+
const assetsPath = getAssetsPath();
|
|
394
|
+
const cutoffDate = new Date();
|
|
395
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
396
|
+
|
|
397
|
+
let deletedCount = 0;
|
|
398
|
+
let freedSpace = 0;
|
|
399
|
+
|
|
400
|
+
const imagePath = path.join(assetsPath, 'images');
|
|
401
|
+
const videoPath = path.join(assetsPath, 'videos');
|
|
402
|
+
|
|
403
|
+
// Clean images
|
|
404
|
+
if (await fs.pathExists(imagePath)) {
|
|
405
|
+
const dateDirs = await fs.readdir(imagePath);
|
|
406
|
+
|
|
407
|
+
for (const dateDir of dateDirs) {
|
|
408
|
+
const dirDate = new Date(dateDir);
|
|
409
|
+
|
|
410
|
+
if (dirDate < cutoffDate) {
|
|
411
|
+
const fullPath = path.join(imagePath, dateDir);
|
|
412
|
+
const files = await fs.readdir(fullPath);
|
|
413
|
+
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
const filePath = path.join(fullPath, file);
|
|
416
|
+
const stats = await fs.stat(filePath);
|
|
417
|
+
freedSpace += stats.size;
|
|
418
|
+
deletedCount++;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await fs.remove(fullPath);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Clean videos
|
|
427
|
+
if (await fs.pathExists(videoPath)) {
|
|
428
|
+
const dateDirs = await fs.readdir(videoPath);
|
|
429
|
+
|
|
430
|
+
for (const dateDir of dateDirs) {
|
|
431
|
+
const dirDate = new Date(dateDir);
|
|
432
|
+
|
|
433
|
+
if (dirDate < cutoffDate) {
|
|
434
|
+
const fullPath = path.join(videoPath, dateDir);
|
|
435
|
+
const files = await fs.readdir(fullPath);
|
|
436
|
+
|
|
437
|
+
for (const file of files) {
|
|
438
|
+
const filePath = path.join(fullPath, file);
|
|
439
|
+
const stats = await fs.stat(filePath);
|
|
440
|
+
freedSpace += stats.size;
|
|
441
|
+
deletedCount++;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await fs.remove(fullPath);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
deletedCount,
|
|
451
|
+
freedSpace,
|
|
452
|
+
freedSpaceMB: (freedSpace / 1024 / 1024).toFixed(2)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Format date as YYYY-MM-DD
|
|
458
|
+
*
|
|
459
|
+
* @param {Date} date - Date to format
|
|
460
|
+
* @returns {string} Formatted date string
|
|
461
|
+
*/
|
|
462
|
+
function formatDate(date) {
|
|
463
|
+
const year = date.getFullYear();
|
|
464
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
465
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
466
|
+
return `${year}-${month}-${day}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* List all generated assets
|
|
471
|
+
*
|
|
472
|
+
* @param {Object} options - Filter options
|
|
473
|
+
* @param {string} options.type - Filter by type (image, video)
|
|
474
|
+
* @param {string} options.date - Filter by date (YYYY-MM-DD)
|
|
475
|
+
* @param {number} options.limit - Limit results
|
|
476
|
+
* @returns {Promise<Array>} List of assets
|
|
477
|
+
*/
|
|
478
|
+
export async function listAssets(options = {}) {
|
|
479
|
+
const { type, date, limit } = options;
|
|
480
|
+
const assetsPath = getAssetsPath();
|
|
481
|
+
const assets = [];
|
|
482
|
+
|
|
483
|
+
// Determine which directories to scan
|
|
484
|
+
const dirs = [];
|
|
485
|
+
if (!type || type === 'image') {
|
|
486
|
+
dirs.push({ type: 'image', path: path.join(assetsPath, 'images') });
|
|
487
|
+
}
|
|
488
|
+
if (!type || type === 'video') {
|
|
489
|
+
dirs.push({ type: 'video', path: path.join(assetsPath, 'videos') });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Scan directories
|
|
493
|
+
for (const dir of dirs) {
|
|
494
|
+
if (!await fs.pathExists(dir.path)) continue;
|
|
495
|
+
|
|
496
|
+
const dateDirs = await fs.readdir(dir.path);
|
|
497
|
+
|
|
498
|
+
for (const dateDir of dateDirs) {
|
|
499
|
+
// Filter by date if specified
|
|
500
|
+
if (date && dateDir !== date) continue;
|
|
501
|
+
|
|
502
|
+
const fullPath = path.join(dir.path, dateDir);
|
|
503
|
+
const files = await fs.readdir(fullPath);
|
|
504
|
+
|
|
505
|
+
for (const file of files) {
|
|
506
|
+
const filePath = path.join(fullPath, file);
|
|
507
|
+
const stats = await fs.stat(filePath);
|
|
508
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
509
|
+
|
|
510
|
+
assets.push({
|
|
511
|
+
type: dir.type,
|
|
512
|
+
filename: file,
|
|
513
|
+
path: filePath,
|
|
514
|
+
relativePath,
|
|
515
|
+
date: dateDir,
|
|
516
|
+
size: stats.size,
|
|
517
|
+
created: stats.birthtime
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Check limit
|
|
521
|
+
if (limit && assets.length >= limit) {
|
|
522
|
+
return assets;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Sort by created date (newest first)
|
|
529
|
+
assets.sort((a, b) => b.created - a.created);
|
|
530
|
+
|
|
531
|
+
return assets;
|
|
532
|
+
}
|