vibecodingmachine-cli 2026.2.20-438 → 2026.2.26-1739
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/bin/auth/auth-compliance.js +126 -0
- package/bin/cli-program.js +104 -0
- package/bin/cli-setup.js +52 -0
- package/bin/commands/agent-commands.js +310 -0
- package/bin/commands/auto-commands.js +70 -0
- package/bin/commands/command-aliases.js +118 -0
- package/bin/commands/repo-commands.js +39 -0
- package/bin/commands/rui-commands.js +152 -0
- package/bin/config/cli-config.js +394 -0
- package/bin/init/environment-setup.js +84 -0
- package/bin/update/update-checker.js +126 -0
- package/bin/vibecodingmachine-new.js +50 -0
- package/bin/vibecodingmachine.js +29 -663
- package/package.json +8 -2
- package/src/commands/agents/add.js +277 -0
- package/src/commands/agents/check.js +380 -0
- package/src/commands/agents/list.js +471 -0
- package/src/commands/agents/remove.js +351 -0
- package/src/commands/analyze-file-sizes.js +428 -0
- package/src/commands/auto-direct/code-processor.js +282 -0
- package/src/commands/auto-direct/file-scanner.js +266 -0
- package/src/commands/auto-direct/provider-config.js +178 -0
- package/src/commands/auto-direct/provider-manager.js +219 -0
- package/src/commands/auto-direct/requirement-manager.js +172 -0
- package/src/commands/auto-direct/status-display.js +91 -0
- package/src/commands/auto-direct/utils.js +106 -0
- package/src/commands/auto-direct.js +875 -488
- package/src/commands/auto-execution.js +342 -0
- package/src/commands/auto-provider-management.js +102 -0
- package/src/commands/auto-requirement-management.js +161 -0
- package/src/commands/auto-status-helpers.js +141 -0
- package/src/commands/auto.js +105 -5155
- package/src/commands/check-compliance.js +536 -0
- package/src/commands/continuous-scan.js +119 -0
- package/src/commands/ide.js +16 -4
- package/src/commands/refactor-file.js +486 -0
- package/src/commands/requirements.js +301 -2
- package/src/commands/timeout.js +290 -0
- package/src/trui/TruiInterface.js +108 -0
- package/src/trui/agents/AgentInterface.js +580 -0
- package/src/utils/antigravity-installer.js +60 -6
- package/src/utils/clarification-actions.js +290 -0
- package/src/utils/config.js +123 -2
- package/src/utils/first-run.js +5 -5
- package/src/utils/ide-handlers.js +212 -0
- package/src/utils/interactive/clarification-actions.js +348 -0
- package/src/utils/interactive/core-ui.js +265 -0
- package/src/utils/interactive/file-backup.js +237 -0
- package/src/utils/interactive/file-import-export.js +305 -0
- package/src/utils/interactive/file-operations.js +49 -0
- package/src/utils/interactive/file-validation.js +276 -0
- package/src/utils/interactive/interactive-prompts.js +480 -0
- package/src/utils/interactive/requirement-actions.js +127 -0
- package/src/utils/interactive/requirement-crud.js +356 -0
- package/src/utils/interactive/requirements-navigation.js +286 -0
- package/src/utils/interactive.js +390 -3459
- package/src/utils/provider-checker/agent-checker.js +250 -0
- package/src/utils/provider-checker/agent-runner.js +450 -0
- package/src/utils/provider-checker/cli-installer.js +123 -0
- package/src/utils/provider-checker/cli-utils.js +15 -0
- package/src/utils/provider-checker/format-utils.js +32 -0
- package/src/utils/provider-checker/ide-manager.js +72 -0
- package/src/utils/provider-checker/ide-utils.js +71 -0
- package/src/utils/provider-checker/node-detector.js +56 -0
- package/src/utils/provider-checker/node-utils.js +61 -0
- package/src/utils/provider-checker/process-spawn.js +22 -0
- package/src/utils/provider-checker/process-utils.js +37 -0
- package/src/utils/provider-checker/provider-validator.js +160 -0
- package/src/utils/provider-checker/quota-checker.js +54 -0
- package/src/utils/provider-checker/quota-detector.js +44 -0
- package/src/utils/provider-checker/requirements-manager.js +94 -0
- package/src/utils/provider-checker/test-requirements.js +95 -0
- package/src/utils/provider-checker/time-formatter.js +18 -0
- package/src/utils/provider-checker-new.js +14 -0
- package/src/utils/provider-checker.js +12 -407
- package/src/utils/provider-checkers/ide-manager.js +128 -0
- package/src/utils/provider-checkers/node-executable-finder.js +51 -0
- package/src/utils/provider-checkers/provider-checker-core.js +172 -0
- package/src/utils/provider-checkers/provider-checker-main.js +107 -0
- package/src/utils/provider-manager.js +60 -4
- package/src/utils/provider-registry.js +26 -3
- package/src/utils/provider-utils.js +173 -0
- package/src/utils/quota-detectors.js +212 -0
- package/src/utils/requirement-action-handlers.js +288 -0
- package/src/utils/requirement-actions/clarification-actions.js +229 -0
- package/src/utils/requirement-actions/confirmation-prompts.js +93 -0
- package/src/utils/requirement-actions/file-operations.js +92 -0
- package/src/utils/requirement-actions/helpers.js +40 -0
- package/src/utils/requirement-actions/requirement-operations.js +335 -0
- package/src/utils/requirement-actions.js +46 -856
- package/src/utils/requirement-file-operations.js +259 -0
- package/src/utils/requirement-helpers.js +128 -0
- package/src/utils/requirement-management.js +279 -0
- package/src/utils/requirement-navigation.js +146 -0
- package/src/utils/requirement-organization.js +271 -0
- package/src/utils/simple-trui.js +75 -1
- package/src/utils/trui-navigation.js +28 -2
- package/src/utils/trui-req-tree.js +196 -11
- package/src/utils/trui-specifications.js +31 -1
- package/src/utils/interactive-backup.js +0 -5664
- package/src/utils/trui-provider-manager.js +0 -182
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
const { t } = require('vibecodingmachine-core');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Translate workflow stage names
|
|
8
|
+
*/
|
|
9
|
+
function translateStage(stage) {
|
|
10
|
+
const stageMap = {
|
|
11
|
+
'PREPARE': 'workflow.stage.prepare',
|
|
12
|
+
'REPRODUCE': 'workflow.stage.reproduce',
|
|
13
|
+
'CREATE UNIT TEST': 'workflow.stage.create.unit.test',
|
|
14
|
+
'ACT': 'workflow.stage.act',
|
|
15
|
+
'CLEAN UP': 'workflow.stage.clean.up',
|
|
16
|
+
'VERIFY': 'workflow.stage.verify',
|
|
17
|
+
'RUN UNIT TESTS': 'workflow.stage.run.unit.tests',
|
|
18
|
+
'DONE': 'workflow.stage.done'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const key = stageMap[stage];
|
|
22
|
+
return key ? t(key) : stage;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeProjectDirName(input) {
|
|
26
|
+
const s = String(input || '').trim().toLowerCase();
|
|
27
|
+
const replaced = s.replace(/\s+/g, '-');
|
|
28
|
+
const cleaned = replaced.replace(/[^a-z0-9._-]/g, '-');
|
|
29
|
+
const collapsed = cleaned.replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
30
|
+
return collapsed || 'my-project';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function bootstrapProjectIfInHomeDir() {
|
|
34
|
+
let cwdResolved = path.resolve(process.cwd());
|
|
35
|
+
let homeResolved = path.resolve(os.homedir());
|
|
36
|
+
|
|
37
|
+
// Check if current directory is under home directory
|
|
38
|
+
if (cwdResolved.startsWith(homeResolved)) {
|
|
39
|
+
const relativePath = path.relative(homeResolved, cwdResolved);
|
|
40
|
+
const projectName = normalizeProjectDirName(relativePath);
|
|
41
|
+
const projectPath = path.join(homeResolved, 'vibe-coding-projects', projectName);
|
|
42
|
+
|
|
43
|
+
console.log(chalk.yellow(`\n⚠️ ${t('interactive.home.dir.warning')}`));
|
|
44
|
+
console.log(chalk.gray(` Current: ${cwdResolved}`));
|
|
45
|
+
console.log(chalk.gray(` Suggested: ${projectPath}`));
|
|
46
|
+
|
|
47
|
+
const { confirm } = require('inquirer');
|
|
48
|
+
const shouldCreate = await confirm({
|
|
49
|
+
message: t('interactive.create.project.dir'),
|
|
50
|
+
default: false
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (shouldCreate) {
|
|
54
|
+
const fs = require('fs-extra');
|
|
55
|
+
await fs.ensureDir(projectPath);
|
|
56
|
+
process.chdir(projectPath);
|
|
57
|
+
console.log(chalk.green(`✅ ${t('interactive.project.created')} ${projectPath}`));
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format IDE name for display
|
|
66
|
+
* @param {string} ide - Internal IDE identifier
|
|
67
|
+
* @returns {string} Display name for IDE
|
|
68
|
+
*/
|
|
69
|
+
function formatIDEName(ide) {
|
|
70
|
+
const ideNames = {
|
|
71
|
+
'claude-code': 'Claude Code CLI',
|
|
72
|
+
'aider': 'Aider CLI',
|
|
73
|
+
'cline': 'Cline CLI',
|
|
74
|
+
'continue': 'Continue CLI',
|
|
75
|
+
'cursor': 'Cursor IDE',
|
|
76
|
+
'windsurf': 'Windsurf IDE',
|
|
77
|
+
'vscode': 'VS Code',
|
|
78
|
+
'replit': 'Replit',
|
|
79
|
+
'github-copilot': 'GitHub Copilot',
|
|
80
|
+
'ollama': 'Ollama',
|
|
81
|
+
'anthropic': 'Anthropic Claude',
|
|
82
|
+
'groq': 'Groq',
|
|
83
|
+
'openai': 'OpenAI GPT'
|
|
84
|
+
};
|
|
85
|
+
return ideNames[ide] || ide;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get current AI provider name for IDEs that require it (like Cline and Continue)
|
|
90
|
+
* @param {string} ide - Internal IDE identifier
|
|
91
|
+
* @returns {string|null} Provider name or null if not applicable/configured
|
|
92
|
+
*/
|
|
93
|
+
function getCurrentAIProvider(ide) {
|
|
94
|
+
// Aider, Cline, and Continue require AI provider configuration
|
|
95
|
+
if (ide !== 'aider' && ide !== 'cline' && ide !== 'continue') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { getAutoConfig } = require('vibecodingmachine-core');
|
|
100
|
+
const config = getAutoConfig();
|
|
101
|
+
|
|
102
|
+
switch (ide) {
|
|
103
|
+
case 'aider':
|
|
104
|
+
return config.aiderModel || 'gpt-4';
|
|
105
|
+
case 'cline':
|
|
106
|
+
return config.anthropicModel || 'claude-3-sonnet-20240229';
|
|
107
|
+
case 'continue':
|
|
108
|
+
return config.anthropicModel || 'claude-3-sonnet-20240229';
|
|
109
|
+
default:
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get unified agent name (IDE or LLM-based)
|
|
116
|
+
* @param {string} agentType - Agent type (e.g., 'cursor', 'ollama', 'anthropic')
|
|
117
|
+
* @returns {string} - Display name like "Cursor IDE Agent" or "Ollama (qwen2.5-coder:32b)"
|
|
118
|
+
*/
|
|
119
|
+
function getAgentDisplayName(agentType) {
|
|
120
|
+
// IDE-based agents
|
|
121
|
+
if (agentType === 'cursor') return 'Cursor IDE Agent';
|
|
122
|
+
if (agentType === 'windsurf') return 'Windsurf IDE Agent';
|
|
123
|
+
if (agentType === 'vscode') return 'VS Code Agent';
|
|
124
|
+
if (agentType === 'claude-code') return 'Claude Code CLI';
|
|
125
|
+
if (agentType === 'aider') return 'Aider CLI';
|
|
126
|
+
if (agentType === 'cline') return 'Cline CLI';
|
|
127
|
+
if (agentType === 'continue') return 'Continue CLI';
|
|
128
|
+
|
|
129
|
+
// LLM-based agents
|
|
130
|
+
const { getAutoConfig } = require('vibecodingmachine-core');
|
|
131
|
+
const config = getAutoConfig();
|
|
132
|
+
|
|
133
|
+
if (agentType === 'ollama') {
|
|
134
|
+
const model = config.llmModel || config.aiderModel || 'llama3';
|
|
135
|
+
return `Ollama (${model})`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (agentType === 'anthropic') {
|
|
139
|
+
const model = config.anthropicModel || 'claude-3-sonnet-20240229';
|
|
140
|
+
return `Anthropic (${model})`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (agentType === 'groq') {
|
|
144
|
+
const model = config.groqModel || 'llama3-70b-8192';
|
|
145
|
+
return `Groq (${model})`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (agentType === 'openai') {
|
|
149
|
+
const model = config.openaiModel || 'gpt-4';
|
|
150
|
+
return `OpenAI (${model})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fallback to IDE formatting
|
|
154
|
+
return formatIDEName(agentType);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatPath(fullPath) {
|
|
158
|
+
const homeDir = os.homedir();
|
|
159
|
+
if (fullPath.startsWith(homeDir)) {
|
|
160
|
+
return fullPath.replace(homeDir, '~');
|
|
161
|
+
}
|
|
162
|
+
return fullPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function countRequirements() {
|
|
166
|
+
try {
|
|
167
|
+
const { getProjectRequirementStats } = require('vibecodingmachine-core');
|
|
168
|
+
const repoPath = await getRepoPath();
|
|
169
|
+
const stats = await getProjectRequirementStats(repoPath);
|
|
170
|
+
return stats;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { todo: 0, completed: 0, total: 0 };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function getRepoPath() {
|
|
177
|
+
const { checkVibeCodingMachineExists } = require('vibecodingmachine-core');
|
|
178
|
+
const repoPath = process.cwd();
|
|
179
|
+
|
|
180
|
+
if (!await checkVibeCodingMachineExists(repoPath)) {
|
|
181
|
+
throw new Error('Not in a Vibe Coding Machine repository');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return repoPath;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function getSyncStatus() {
|
|
188
|
+
try {
|
|
189
|
+
const SyncEngine = require('vibecodingmachine-core/src/sync/sync-engine');
|
|
190
|
+
const syncEngine = new SyncEngine();
|
|
191
|
+
const status = await syncEngine.getStatus();
|
|
192
|
+
return status;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return { status: 'unknown', message: 'Sync engine not available' };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function getCurrentProgress() {
|
|
199
|
+
try {
|
|
200
|
+
const { getRequirementsPath } = require('vibecodingmachine-core');
|
|
201
|
+
const reqPath = await getRequirementsPath();
|
|
202
|
+
const fs = require('fs-extra');
|
|
203
|
+
|
|
204
|
+
if (!await fs.pathExists(reqPath)) {
|
|
205
|
+
return { stage: 'NOT_STARTED', progress: 0 };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const content = await fs.readFile(reqPath, 'utf8');
|
|
209
|
+
const stageMatch = content.match(/## 🚦 Current Status\s*\n\s*\n(.+)/);
|
|
210
|
+
|
|
211
|
+
if (stageMatch) {
|
|
212
|
+
const stage = stageMatch[1].trim();
|
|
213
|
+
const progress = getProgressPercentage(stage);
|
|
214
|
+
return { stage, progress };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { stage: 'UNKNOWN', progress: 0 };
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return { stage: 'ERROR', progress: 0 };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getProgressPercentage(stage) {
|
|
224
|
+
const stageProgress = {
|
|
225
|
+
'PREPARE': 10,
|
|
226
|
+
'REPRODUCE': 20,
|
|
227
|
+
'CREATE UNIT TEST': 30,
|
|
228
|
+
'ACT': 50,
|
|
229
|
+
'CLEAN UP': 80,
|
|
230
|
+
'VERIFY': 90,
|
|
231
|
+
'DONE': 100
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return stageProgress[stage] || 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Helper to show goodbye message
|
|
238
|
+
function showGoodbyeMessage() {
|
|
239
|
+
const hour = new Date().getHours();
|
|
240
|
+
const message = hour < 21
|
|
241
|
+
? '\n👋 ' + t('interactive.goodbye') + '\n'
|
|
242
|
+
: '\n🌙 ' + t('interactive.goodbye.night') + '\n';
|
|
243
|
+
console.log(chalk.cyan(message));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Helper to convert index to letter (0->a, 1->b, etc.)
|
|
247
|
+
function indexToLetter(index) {
|
|
248
|
+
return String.fromCharCode(97 + index); // 97 is 'a'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = {
|
|
252
|
+
translateStage,
|
|
253
|
+
normalizeProjectDirName,
|
|
254
|
+
bootstrapProjectIfInHomeDir,
|
|
255
|
+
formatIDEName,
|
|
256
|
+
getCurrentAIProvider,
|
|
257
|
+
getAgentDisplayName,
|
|
258
|
+
formatPath,
|
|
259
|
+
countRequirements,
|
|
260
|
+
getRepoPath,
|
|
261
|
+
getSyncStatus,
|
|
262
|
+
getCurrentProgress,
|
|
263
|
+
showGoodbyeMessage,
|
|
264
|
+
indexToLetter
|
|
265
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Backup Operations
|
|
3
|
+
*
|
|
4
|
+
* Handles backup creation, restoration, and management for requirements files
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs').promises;
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a backup of the specified file
|
|
13
|
+
* @param {string} filePath - Path to the file to backup
|
|
14
|
+
* @param {Object} options - Backup options
|
|
15
|
+
* @returns {Promise<Object>} Backup result
|
|
16
|
+
*/
|
|
17
|
+
async function createBackup(filePath, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
const backupDir = options.backupDir || path.join(path.dirname(filePath), '.backups');
|
|
20
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
21
|
+
const fileName = path.basename(filePath);
|
|
22
|
+
const backupFileName = `${fileName}.${timestamp}.backup`;
|
|
23
|
+
const backupPath = path.join(backupDir, backupFileName);
|
|
24
|
+
|
|
25
|
+
// Ensure backup directory exists
|
|
26
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
// Check if source file exists
|
|
29
|
+
const sourceExists = await fs.access(filePath).then(() => true).catch(() => false);
|
|
30
|
+
if (!sourceExists) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: `Source file not found: ${filePath}`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Copy file to backup location
|
|
38
|
+
await fs.copyFile(filePath, backupPath);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
backupPath,
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
originalPath: filePath
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: error.message
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Restore from a backup file
|
|
56
|
+
* @param {string} backupPath - Path to the backup file
|
|
57
|
+
* @param {string} targetPath - Path where to restore the file
|
|
58
|
+
* @returns {Promise<Object>} Restore result
|
|
59
|
+
*/
|
|
60
|
+
async function restoreFromBackup(backupPath, targetPath) {
|
|
61
|
+
try {
|
|
62
|
+
// Check if backup file exists
|
|
63
|
+
const backupExists = await fs.access(backupPath).then(() => true).catch(() => false);
|
|
64
|
+
if (!backupExists) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: `Backup file not found: ${backupPath}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Create backup of current file before restoring
|
|
72
|
+
const currentBackup = await createBackup(targetPath, {
|
|
73
|
+
backupDir: path.join(path.dirname(targetPath), '.pre-restore-backups')
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Restore from backup
|
|
77
|
+
await fs.copyFile(backupPath, targetPath);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
backupPath,
|
|
82
|
+
targetPath,
|
|
83
|
+
preRestoreBackup: currentBackup.success ? currentBackup.backupPath : null
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: error.message
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* List all available backups for a file
|
|
95
|
+
* @param {string} filePath - Path to the original file
|
|
96
|
+
* @param {Object} options - Listing options
|
|
97
|
+
* @returns {Promise<Object>} List of backups
|
|
98
|
+
*/
|
|
99
|
+
async function listBackups(filePath, options = {}) {
|
|
100
|
+
try {
|
|
101
|
+
const backupDir = options.backupDir || path.join(path.dirname(filePath), '.backups');
|
|
102
|
+
const fileName = path.basename(filePath);
|
|
103
|
+
|
|
104
|
+
// Check if backup directory exists
|
|
105
|
+
const dirExists = await fs.access(backupDir).then(() => true).catch(() => false);
|
|
106
|
+
if (!dirExists) {
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
backups: []
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Read backup directory
|
|
114
|
+
const files = await fs.readdir(backupDir);
|
|
115
|
+
const backupFiles = files
|
|
116
|
+
.filter(file => file.startsWith(fileName) && file.endsWith('.backup'))
|
|
117
|
+
.map(file => {
|
|
118
|
+
const filePath = path.join(backupDir, file);
|
|
119
|
+
return {
|
|
120
|
+
fileName: file,
|
|
121
|
+
filePath,
|
|
122
|
+
timestamp: extractTimestampFromFileName(file),
|
|
123
|
+
size: null // Will be populated if detailed option is true
|
|
124
|
+
};
|
|
125
|
+
})
|
|
126
|
+
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
127
|
+
|
|
128
|
+
// Get file sizes if detailed option is true
|
|
129
|
+
if (options.detailed) {
|
|
130
|
+
for (const backup of backupFiles) {
|
|
131
|
+
try {
|
|
132
|
+
const stats = await fs.stat(backup.filePath);
|
|
133
|
+
backup.size = stats.size;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
backup.size = -1; // Error getting size
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
backups: backupFiles
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
error: error.message
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clean up old backups
|
|
154
|
+
* @param {string} filePath - Path to the original file
|
|
155
|
+
* @param {Object} options - Cleanup options
|
|
156
|
+
* @returns {Promise<Object>} Cleanup result
|
|
157
|
+
*/
|
|
158
|
+
async function cleanupOldBackups(filePath, options = {}) {
|
|
159
|
+
try {
|
|
160
|
+
const maxBackups = options.maxBackups || 10;
|
|
161
|
+
const olderThan = options.olderThan || null; // Date or days
|
|
162
|
+
const backupDir = options.backupDir || path.join(path.dirname(filePath), '.backups');
|
|
163
|
+
|
|
164
|
+
const listResult = await listBackups(filePath, { backupDir });
|
|
165
|
+
if (!listResult.success) {
|
|
166
|
+
return listResult;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { backups } = listResult;
|
|
170
|
+
if (backups.length <= maxBackups) {
|
|
171
|
+
return {
|
|
172
|
+
success: true,
|
|
173
|
+
deletedBackups: [],
|
|
174
|
+
message: `No cleanup needed. ${backups.length} backups (limit: ${maxBackups})`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Determine which backups to delete
|
|
179
|
+
const backupsToDelete = [];
|
|
180
|
+
|
|
181
|
+
if (olderThan) {
|
|
182
|
+
// Delete backups older than specified date
|
|
183
|
+
const cutoffDate = typeof olderThan === 'number'
|
|
184
|
+
? new Date(Date.now() - olderThan * 24 * 60 * 60 * 1000) // days
|
|
185
|
+
: new Date(olderThan);
|
|
186
|
+
|
|
187
|
+
backupsToDelete.push(...backups.filter(backup =>
|
|
188
|
+
new Date(backup.timestamp) < cutoffDate
|
|
189
|
+
));
|
|
190
|
+
} else {
|
|
191
|
+
// Keep only the most recent maxBackups
|
|
192
|
+
backupsToDelete.push(...backups.slice(maxBackups));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Delete old backups
|
|
196
|
+
const deletedBackups = [];
|
|
197
|
+
for (const backup of backupsToDelete) {
|
|
198
|
+
try {
|
|
199
|
+
await fs.unlink(backup.filePath);
|
|
200
|
+
deletedBackups.push(backup);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.warn(chalk.yellow(`Warning: Failed to delete backup ${backup.fileName}: ${error.message}`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
deletedBackups,
|
|
209
|
+
remainingBackups: backups.length - deletedBackups.length
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: error.message
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Extract timestamp from backup filename
|
|
221
|
+
* @param {string} fileName - Backup filename
|
|
222
|
+
* @returns {string} ISO timestamp
|
|
223
|
+
*/
|
|
224
|
+
function extractTimestampFromFileName(fileName) {
|
|
225
|
+
const match = fileName.match(/\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)\.backup$/);
|
|
226
|
+
if (match) {
|
|
227
|
+
return match[1].replace(/-/g, ':').replace(/(\d{2}):(\d{2}):(\d{2})-(\d{3})/, '$1:$2:$3.$4');
|
|
228
|
+
}
|
|
229
|
+
return fileName;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
createBackup,
|
|
234
|
+
restoreFromBackup,
|
|
235
|
+
listBackups,
|
|
236
|
+
cleanupOldBackups
|
|
237
|
+
};
|