@nclamvn/vibecode-cli 1.5.0 → 1.7.0
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/.vibecode/learning/fixes.json +1 -0
- package/.vibecode/learning/preferences.json +1 -0
- package/bin/vibecode.js +86 -3
- package/docs-site/README.md +41 -0
- package/docs-site/blog/2019-05-28-first-blog-post.md +12 -0
- package/docs-site/blog/2019-05-29-long-blog-post.md +44 -0
- package/docs-site/blog/2021-08-01-mdx-blog-post.mdx +24 -0
- package/docs-site/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs-site/blog/2021-08-26-welcome/index.md +29 -0
- package/docs-site/blog/authors.yml +25 -0
- package/docs-site/blog/tags.yml +19 -0
- package/docs-site/docs/commands/agent.md +162 -0
- package/docs-site/docs/commands/assist.md +71 -0
- package/docs-site/docs/commands/build.md +53 -0
- package/docs-site/docs/commands/config.md +30 -0
- package/docs-site/docs/commands/debug.md +173 -0
- package/docs-site/docs/commands/doctor.md +34 -0
- package/docs-site/docs/commands/go.md +128 -0
- package/docs-site/docs/commands/index.md +79 -0
- package/docs-site/docs/commands/init.md +42 -0
- package/docs-site/docs/commands/learn.md +82 -0
- package/docs-site/docs/commands/lock.md +33 -0
- package/docs-site/docs/commands/plan.md +29 -0
- package/docs-site/docs/commands/review.md +31 -0
- package/docs-site/docs/commands/snapshot.md +34 -0
- package/docs-site/docs/commands/start.md +32 -0
- package/docs-site/docs/commands/status.md +37 -0
- package/docs-site/docs/commands/undo.md +83 -0
- package/docs-site/docs/configuration.md +72 -0
- package/docs-site/docs/faq.md +83 -0
- package/docs-site/docs/getting-started.md +119 -0
- package/docs-site/docs/guides/agent-mode.md +94 -0
- package/docs-site/docs/guides/debug-mode.md +83 -0
- package/docs-site/docs/guides/magic-mode.md +107 -0
- package/docs-site/docs/installation.md +98 -0
- package/docs-site/docs/intro.md +67 -0
- package/docs-site/docusaurus.config.ts +141 -0
- package/docs-site/package-lock.json +18039 -0
- package/docs-site/package.json +48 -0
- package/docs-site/sidebars.ts +70 -0
- package/docs-site/src/components/HomepageFeatures/index.tsx +72 -0
- package/docs-site/src/components/HomepageFeatures/styles.module.css +16 -0
- package/docs-site/src/css/custom.css +30 -0
- package/docs-site/src/pages/index.module.css +23 -0
- package/docs-site/src/pages/index.tsx +44 -0
- package/docs-site/src/pages/markdown-page.md +7 -0
- package/docs-site/src/theme/Footer/index.tsx +127 -0
- package/docs-site/src/theme/Footer/styles.module.css +285 -0
- package/docs-site/static/.nojekyll +0 -0
- package/docs-site/static/img/docusaurus-social-card.jpg +0 -0
- package/docs-site/static/img/docusaurus.png +0 -0
- package/docs-site/static/img/favicon.ico +0 -0
- package/docs-site/static/img/logo.svg +1 -0
- package/docs-site/static/img/undraw_docusaurus_mountain.svg +171 -0
- package/docs-site/static/img/undraw_docusaurus_react.svg +170 -0
- package/docs-site/static/img/undraw_docusaurus_tree.svg +40 -0
- package/docs-site/tsconfig.json +8 -0
- package/package.json +5 -2
- package/src/agent/orchestrator.js +104 -35
- package/src/commands/build.js +13 -3
- package/src/commands/debug.js +109 -1
- package/src/commands/git.js +923 -0
- package/src/commands/go.js +9 -2
- package/src/commands/learn.js +294 -0
- package/src/commands/shell.js +486 -0
- package/src/commands/undo.js +281 -0
- package/src/commands/watch.js +556 -0
- package/src/commands/wizard.js +322 -0
- package/src/core/backup.js +325 -0
- package/src/core/learning.js +295 -0
- package/src/debug/image-analyzer.js +304 -0
- package/src/debug/index.js +30 -1
- package/src/index.js +50 -0
- package/src/ui/__tests__/error-translator.test.js +390 -0
- package/src/ui/dashboard.js +364 -0
- package/src/ui/error-translator.js +775 -0
- package/src/utils/image.js +222 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Interactive Wizard
|
|
3
|
+
// Phase H1: Smart Defaults - No args = Interactive menu
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { VERSION } from '../config/constants.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Wizard Command - Interactive menu when no args provided
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* vibecode (no args) → Shows interactive menu
|
|
15
|
+
*/
|
|
16
|
+
export async function wizardCommand() {
|
|
17
|
+
// Show welcome banner
|
|
18
|
+
console.log(chalk.cyan(`
|
|
19
|
+
╭────────────────────────────────────────────────────────────────────╮
|
|
20
|
+
│ │
|
|
21
|
+
│ 🏗️ VIBECODE v${VERSION.padEnd(43)}│
|
|
22
|
+
│ Build Software with Discipline │
|
|
23
|
+
│ │
|
|
24
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
25
|
+
`));
|
|
26
|
+
|
|
27
|
+
const { action } = await inquirer.prompt([
|
|
28
|
+
{
|
|
29
|
+
type: 'list',
|
|
30
|
+
name: 'action',
|
|
31
|
+
message: 'Bạn muốn làm gì hôm nay?',
|
|
32
|
+
choices: [
|
|
33
|
+
{ name: '🚀 Tạo project mới nhanh', value: 'go' },
|
|
34
|
+
{ name: '🤖 Build project phức tạp (nhiều modules)', value: 'agent' },
|
|
35
|
+
{ name: '🔍 Debug/fix lỗi', value: 'debug' },
|
|
36
|
+
{ name: '🤝 Trợ giúp từ AI', value: 'assist' },
|
|
37
|
+
new inquirer.Separator(),
|
|
38
|
+
{ name: '📊 Xem trạng thái project hiện tại', value: 'status' },
|
|
39
|
+
{ name: '⚙️ Cài đặt', value: 'config' },
|
|
40
|
+
{ name: '📁 Khởi tạo workspace mới', value: 'init' },
|
|
41
|
+
new inquirer.Separator(),
|
|
42
|
+
{ name: '❓ Xem trợ giúp', value: 'help' },
|
|
43
|
+
{ name: '👋 Thoát', value: 'exit' }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
switch (action) {
|
|
49
|
+
case 'go':
|
|
50
|
+
await handleGo();
|
|
51
|
+
break;
|
|
52
|
+
case 'agent':
|
|
53
|
+
await handleAgent();
|
|
54
|
+
break;
|
|
55
|
+
case 'debug':
|
|
56
|
+
await handleDebug();
|
|
57
|
+
break;
|
|
58
|
+
case 'assist':
|
|
59
|
+
await handleAssist();
|
|
60
|
+
break;
|
|
61
|
+
case 'status':
|
|
62
|
+
await handleStatus();
|
|
63
|
+
break;
|
|
64
|
+
case 'config':
|
|
65
|
+
await handleConfig();
|
|
66
|
+
break;
|
|
67
|
+
case 'init':
|
|
68
|
+
await handleInit();
|
|
69
|
+
break;
|
|
70
|
+
case 'help':
|
|
71
|
+
showHelp();
|
|
72
|
+
break;
|
|
73
|
+
case 'exit':
|
|
74
|
+
console.log(chalk.cyan('\n👋 Hẹn gặp lại!\n'));
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Handle Go command with prompts
|
|
81
|
+
*/
|
|
82
|
+
async function handleGo() {
|
|
83
|
+
const { goCommand } = await import('./go.js');
|
|
84
|
+
|
|
85
|
+
const { description } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'description',
|
|
89
|
+
message: 'Mô tả project bạn muốn tạo:',
|
|
90
|
+
validate: (input) => input.length > 5 || 'Vui lòng mô tả chi tiết hơn (ít nhất 6 ký tự)'
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const { template } = await inquirer.prompt([
|
|
95
|
+
{
|
|
96
|
+
type: 'list',
|
|
97
|
+
name: 'template',
|
|
98
|
+
message: 'Chọn template (hoặc để trống):',
|
|
99
|
+
choices: [
|
|
100
|
+
{ name: '🎨 Không dùng template (tự do)', value: null },
|
|
101
|
+
{ name: '🌐 Landing page', value: 'landing' },
|
|
102
|
+
{ name: '💼 SaaS application', value: 'saas' },
|
|
103
|
+
{ name: '⌨️ CLI tool', value: 'cli' },
|
|
104
|
+
{ name: '🔌 REST API', value: 'api' }
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const { confirm } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: 'confirm',
|
|
112
|
+
name: 'confirm',
|
|
113
|
+
message: `Tạo project: "${description}"${template ? ` (template: ${template})` : ''}?`,
|
|
114
|
+
default: true
|
|
115
|
+
}
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (confirm) {
|
|
119
|
+
const options = {};
|
|
120
|
+
if (template) options.template = template;
|
|
121
|
+
await goCommand(description, options);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(chalk.gray('\n✗ Đã huỷ.\n'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle Agent command with prompts
|
|
129
|
+
*/
|
|
130
|
+
async function handleAgent() {
|
|
131
|
+
const { agentCommand } = await import('./agent.js');
|
|
132
|
+
|
|
133
|
+
const { description } = await inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: 'input',
|
|
136
|
+
name: 'description',
|
|
137
|
+
message: 'Mô tả project phức tạp (VD: SaaS với auth, billing, dashboard):',
|
|
138
|
+
validate: (input) => input.length > 10 || 'Vui lòng mô tả chi tiết hơn (ít nhất 11 ký tự)'
|
|
139
|
+
}
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const { createNew } = await inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'confirm',
|
|
145
|
+
name: 'createNew',
|
|
146
|
+
message: 'Tạo thư mục project mới?',
|
|
147
|
+
default: true
|
|
148
|
+
}
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
const { verbose } = await inquirer.prompt([
|
|
152
|
+
{
|
|
153
|
+
type: 'confirm',
|
|
154
|
+
name: 'verbose',
|
|
155
|
+
message: 'Hiển thị chi tiết quá trình build?',
|
|
156
|
+
default: false
|
|
157
|
+
}
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
console.log(chalk.blue('\n🤖 Bắt đầu Agent Mode...\n'));
|
|
161
|
+
await agentCommand(description, { new: createNew, verbose });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Handle Debug command
|
|
166
|
+
*/
|
|
167
|
+
async function handleDebug() {
|
|
168
|
+
const { debugCommand } = await import('./debug.js');
|
|
169
|
+
|
|
170
|
+
const { mode } = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'list',
|
|
173
|
+
name: 'mode',
|
|
174
|
+
message: 'Chọn chế độ debug:',
|
|
175
|
+
choices: [
|
|
176
|
+
{ name: '🔄 Interactive - Debug trực tiếp', value: 'interactive' },
|
|
177
|
+
{ name: '🔍 Auto-scan - Tự động tìm và fix lỗi', value: 'auto' },
|
|
178
|
+
{ name: '📝 Mô tả lỗi', value: 'describe' }
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
if (mode === 'interactive') {
|
|
184
|
+
await debugCommand([], { interactive: true });
|
|
185
|
+
} else if (mode === 'auto') {
|
|
186
|
+
await debugCommand([], { auto: true });
|
|
187
|
+
} else {
|
|
188
|
+
const { description } = await inquirer.prompt([
|
|
189
|
+
{
|
|
190
|
+
type: 'input',
|
|
191
|
+
name: 'description',
|
|
192
|
+
message: 'Mô tả lỗi bạn gặp phải:',
|
|
193
|
+
validate: (input) => input.length > 5 || 'Vui lòng mô tả chi tiết hơn'
|
|
194
|
+
}
|
|
195
|
+
]);
|
|
196
|
+
await debugCommand([description], {});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle Assist command
|
|
202
|
+
*/
|
|
203
|
+
async function handleAssist() {
|
|
204
|
+
const { assistCommand } = await import('./assist.js');
|
|
205
|
+
|
|
206
|
+
const { hasPrompt } = await inquirer.prompt([
|
|
207
|
+
{
|
|
208
|
+
type: 'confirm',
|
|
209
|
+
name: 'hasPrompt',
|
|
210
|
+
message: 'Bạn có câu hỏi cụ thể không?',
|
|
211
|
+
default: false
|
|
212
|
+
}
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
if (hasPrompt) {
|
|
216
|
+
const { prompt } = await inquirer.prompt([
|
|
217
|
+
{
|
|
218
|
+
type: 'input',
|
|
219
|
+
name: 'prompt',
|
|
220
|
+
message: 'Nhập câu hỏi của bạn:',
|
|
221
|
+
validate: (input) => input.length > 3 || 'Vui lòng nhập câu hỏi'
|
|
222
|
+
}
|
|
223
|
+
]);
|
|
224
|
+
await assistCommand([prompt], {});
|
|
225
|
+
} else {
|
|
226
|
+
await assistCommand([], {});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Handle Status command
|
|
232
|
+
*/
|
|
233
|
+
async function handleStatus() {
|
|
234
|
+
const { statusCommand } = await import('./status.js');
|
|
235
|
+
await statusCommand({});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Handle Config command
|
|
240
|
+
*/
|
|
241
|
+
async function handleConfig() {
|
|
242
|
+
const { configCommand } = await import('./config.js');
|
|
243
|
+
|
|
244
|
+
const { action } = await inquirer.prompt([
|
|
245
|
+
{
|
|
246
|
+
type: 'list',
|
|
247
|
+
name: 'action',
|
|
248
|
+
message: 'Cài đặt:',
|
|
249
|
+
choices: [
|
|
250
|
+
{ name: '👀 Xem cài đặt hiện tại', value: 'show' },
|
|
251
|
+
{ name: '🔧 Thay đổi provider', value: 'provider' }
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
if (action === 'show') {
|
|
257
|
+
await configCommand({ show: true });
|
|
258
|
+
} else {
|
|
259
|
+
const { provider } = await inquirer.prompt([
|
|
260
|
+
{
|
|
261
|
+
type: 'list',
|
|
262
|
+
name: 'provider',
|
|
263
|
+
message: 'Chọn AI provider:',
|
|
264
|
+
choices: [
|
|
265
|
+
{ name: 'Claude Code (mặc định)', value: 'claude-code' }
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
]);
|
|
269
|
+
await configCommand({ provider });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handle Init command
|
|
275
|
+
*/
|
|
276
|
+
async function handleInit() {
|
|
277
|
+
const { initCommand } = await import('./init.js');
|
|
278
|
+
|
|
279
|
+
const { confirm } = await inquirer.prompt([
|
|
280
|
+
{
|
|
281
|
+
type: 'confirm',
|
|
282
|
+
name: 'confirm',
|
|
283
|
+
message: `Khởi tạo Vibecode workspace tại ${process.cwd()}?`,
|
|
284
|
+
default: true
|
|
285
|
+
}
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
if (confirm) {
|
|
289
|
+
await initCommand({});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Show help text
|
|
295
|
+
*/
|
|
296
|
+
function showHelp() {
|
|
297
|
+
console.log(chalk.white(`
|
|
298
|
+
📚 VIBECODE COMMANDS
|
|
299
|
+
|
|
300
|
+
${chalk.cyan('Workflow cơ bản:')}
|
|
301
|
+
vibecode init Khởi tạo workspace
|
|
302
|
+
vibecode start Bắt đầu intake → blueprint → contract
|
|
303
|
+
vibecode lock Khoá contract
|
|
304
|
+
vibecode plan Tạo execution plan
|
|
305
|
+
vibecode build Build với AI
|
|
306
|
+
vibecode review Kiểm tra kết quả
|
|
307
|
+
vibecode snapshot Tạo release
|
|
308
|
+
|
|
309
|
+
${chalk.cyan('Power commands:')}
|
|
310
|
+
vibecode go "..." Một lệnh, tạo cả project
|
|
311
|
+
vibecode agent "..." Build project phức tạp tự động
|
|
312
|
+
vibecode debug Debug thông minh 9 bước
|
|
313
|
+
vibecode assist Trợ giúp trực tiếp từ AI
|
|
314
|
+
|
|
315
|
+
${chalk.cyan('Khác:')}
|
|
316
|
+
vibecode status Xem trạng thái
|
|
317
|
+
vibecode config Cài đặt
|
|
318
|
+
vibecode doctor Kiểm tra health
|
|
319
|
+
|
|
320
|
+
📖 Chi tiết: ${chalk.yellow('vibecode <command> --help')}
|
|
321
|
+
`));
|
|
322
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// VIBECODE CLI - Backup Manager
|
|
3
|
+
// Phase H4: Undo/Rollback - Auto-backup before every action
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
|
|
10
|
+
const BACKUP_DIR = '.vibecode/backups';
|
|
11
|
+
const MAX_BACKUPS = 10;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* BackupManager - Creates and manages backups for undo functionality
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* const backup = new BackupManager();
|
|
18
|
+
* const id = await backup.createBackup('build');
|
|
19
|
+
* // ... do stuff ...
|
|
20
|
+
* await backup.restore(id); // Undo!
|
|
21
|
+
*/
|
|
22
|
+
export class BackupManager {
|
|
23
|
+
constructor(projectPath = process.cwd()) {
|
|
24
|
+
this.projectPath = projectPath;
|
|
25
|
+
this.backupPath = path.join(projectPath, BACKUP_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize backup directory
|
|
30
|
+
*/
|
|
31
|
+
async init() {
|
|
32
|
+
await fs.ensureDir(this.backupPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create backup before action
|
|
37
|
+
* @param {string} actionName - Name of action (build, agent, go, etc.)
|
|
38
|
+
* @param {string[]} files - Specific files to backup (optional)
|
|
39
|
+
* @returns {string} Backup ID
|
|
40
|
+
*/
|
|
41
|
+
async createBackup(actionName, files = null) {
|
|
42
|
+
await this.init();
|
|
43
|
+
|
|
44
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
45
|
+
const backupId = `${timestamp}_${actionName}`;
|
|
46
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
47
|
+
|
|
48
|
+
await fs.ensureDir(backupDir);
|
|
49
|
+
|
|
50
|
+
// If specific files provided, backup only those
|
|
51
|
+
// Otherwise, backup common source files
|
|
52
|
+
const filesToBackup = files || await this.getSourceFiles();
|
|
53
|
+
|
|
54
|
+
const manifest = {
|
|
55
|
+
id: backupId,
|
|
56
|
+
action: actionName,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
files: []
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const file of filesToBackup) {
|
|
62
|
+
try {
|
|
63
|
+
const sourcePath = path.join(this.projectPath, file);
|
|
64
|
+
const stats = await fs.stat(sourcePath).catch(() => null);
|
|
65
|
+
|
|
66
|
+
if (stats && stats.isFile()) {
|
|
67
|
+
const content = await fs.readFile(sourcePath);
|
|
68
|
+
const hash = createHash('md5').update(content).digest('hex');
|
|
69
|
+
|
|
70
|
+
// Replace slashes with __ for flat storage
|
|
71
|
+
const backupFileName = file.replace(/[/\\]/g, '__');
|
|
72
|
+
const backupFilePath = path.join(backupDir, backupFileName);
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(backupFilePath, content);
|
|
75
|
+
|
|
76
|
+
manifest.files.push({
|
|
77
|
+
path: file,
|
|
78
|
+
hash,
|
|
79
|
+
size: stats.size,
|
|
80
|
+
backupName: backupFileName
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Skip files that can't be backed up
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Save manifest
|
|
89
|
+
await fs.writeFile(
|
|
90
|
+
path.join(backupDir, 'manifest.json'),
|
|
91
|
+
JSON.stringify(manifest, null, 2)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Cleanup old backups
|
|
95
|
+
await this.cleanupOldBackups();
|
|
96
|
+
|
|
97
|
+
return backupId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get list of source files to backup
|
|
102
|
+
* @returns {string[]} File paths relative to project
|
|
103
|
+
*/
|
|
104
|
+
async getSourceFiles() {
|
|
105
|
+
const files = [];
|
|
106
|
+
const ignoreDirs = [
|
|
107
|
+
'node_modules', '.git', '.next', 'dist', 'build',
|
|
108
|
+
'.vibecode/backups', 'coverage', '.cache', '__pycache__'
|
|
109
|
+
];
|
|
110
|
+
const extensions = [
|
|
111
|
+
'.js', '.ts', '.tsx', '.jsx', '.json', '.css', '.scss',
|
|
112
|
+
'.html', '.md', '.vue', '.svelte', '.prisma', '.env'
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const scan = async (dir, prefix = '') => {
|
|
116
|
+
try {
|
|
117
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const fullPath = path.join(dir, entry.name);
|
|
121
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
122
|
+
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith('.')) {
|
|
125
|
+
await scan(fullPath, relativePath);
|
|
126
|
+
}
|
|
127
|
+
} else if (entry.isFile()) {
|
|
128
|
+
const ext = path.extname(entry.name);
|
|
129
|
+
if (extensions.includes(ext)) {
|
|
130
|
+
files.push(relativePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Skip directories that can't be read
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await scan(this.projectPath);
|
|
140
|
+
return files.slice(0, 100); // Limit to 100 files
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* List available backups
|
|
145
|
+
* @returns {Object[]} Array of backup manifests
|
|
146
|
+
*/
|
|
147
|
+
async listBackups() {
|
|
148
|
+
await this.init();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const entries = await fs.readdir(this.backupPath, { withFileTypes: true });
|
|
152
|
+
const backups = [];
|
|
153
|
+
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
const manifestPath = path.join(this.backupPath, entry.name, 'manifest.json');
|
|
157
|
+
try {
|
|
158
|
+
const manifest = await fs.readJson(manifestPath);
|
|
159
|
+
backups.push(manifest);
|
|
160
|
+
} catch {
|
|
161
|
+
// Skip invalid backups
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by timestamp, newest first
|
|
167
|
+
return backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
168
|
+
} catch {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Restore from backup
|
|
175
|
+
* @param {string} backupId - Backup ID to restore
|
|
176
|
+
* @returns {Object} Restore result
|
|
177
|
+
*/
|
|
178
|
+
async restore(backupId) {
|
|
179
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
180
|
+
const manifestPath = path.join(backupDir, 'manifest.json');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const manifest = await fs.readJson(manifestPath);
|
|
184
|
+
const restored = [];
|
|
185
|
+
|
|
186
|
+
for (const file of manifest.files) {
|
|
187
|
+
const backupFilePath = path.join(backupDir, file.backupName);
|
|
188
|
+
const targetPath = path.join(this.projectPath, file.path);
|
|
189
|
+
|
|
190
|
+
// Ensure directory exists
|
|
191
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
192
|
+
|
|
193
|
+
// Restore file
|
|
194
|
+
const content = await fs.readFile(backupFilePath);
|
|
195
|
+
await fs.writeFile(targetPath, content);
|
|
196
|
+
|
|
197
|
+
restored.push(file.path);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
backupId,
|
|
203
|
+
action: manifest.action,
|
|
204
|
+
timestamp: manifest.timestamp,
|
|
205
|
+
filesRestored: restored.length,
|
|
206
|
+
files: restored
|
|
207
|
+
};
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
error: error.message
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Restore to N steps ago
|
|
218
|
+
* @param {number} steps - Number of steps back
|
|
219
|
+
* @returns {Object} Restore result
|
|
220
|
+
*/
|
|
221
|
+
async restoreSteps(steps = 1) {
|
|
222
|
+
const backups = await this.listBackups();
|
|
223
|
+
|
|
224
|
+
if (steps > backups.length) {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
error: `Only ${backups.length} backups available`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const backup = backups[steps - 1];
|
|
232
|
+
return await this.restore(backup.id);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the most recent backup
|
|
237
|
+
* @returns {Object|null} Latest backup manifest or null
|
|
238
|
+
*/
|
|
239
|
+
async getLatestBackup() {
|
|
240
|
+
const backups = await this.listBackups();
|
|
241
|
+
return backups.length > 0 ? backups[0] : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Cleanup old backups to maintain MAX_BACKUPS limit
|
|
246
|
+
*/
|
|
247
|
+
async cleanupOldBackups() {
|
|
248
|
+
const backups = await this.listBackups();
|
|
249
|
+
|
|
250
|
+
if (backups.length > MAX_BACKUPS) {
|
|
251
|
+
const toDelete = backups.slice(MAX_BACKUPS);
|
|
252
|
+
|
|
253
|
+
for (const backup of toDelete) {
|
|
254
|
+
const backupDir = path.join(this.backupPath, backup.id);
|
|
255
|
+
await fs.remove(backupDir);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Delete specific backup
|
|
262
|
+
* @param {string} backupId - Backup ID to delete
|
|
263
|
+
*/
|
|
264
|
+
async deleteBackup(backupId) {
|
|
265
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
266
|
+
await fs.remove(backupDir);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear all backups
|
|
271
|
+
*/
|
|
272
|
+
async clearAllBackups() {
|
|
273
|
+
await fs.remove(this.backupPath);
|
|
274
|
+
await this.init();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get backup size in bytes
|
|
279
|
+
* @param {string} backupId - Backup ID
|
|
280
|
+
* @returns {number} Size in bytes
|
|
281
|
+
*/
|
|
282
|
+
async getBackupSize(backupId) {
|
|
283
|
+
const backupDir = path.join(this.backupPath, backupId);
|
|
284
|
+
let totalSize = 0;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const files = await fs.readdir(backupDir);
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
const stats = await fs.stat(path.join(backupDir, file));
|
|
290
|
+
totalSize += stats.size;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// Ignore errors
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return totalSize;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Helper to wrap action with auto-backup
|
|
302
|
+
* @param {string} actionName - Name of action
|
|
303
|
+
* @param {Function} fn - Async function to execute
|
|
304
|
+
* @returns {*} Result of fn()
|
|
305
|
+
*/
|
|
306
|
+
export async function withBackup(actionName, fn) {
|
|
307
|
+
const backup = new BackupManager();
|
|
308
|
+
const backupId = await backup.createBackup(actionName);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return await fn();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// Error occurred - backup is available for restore
|
|
314
|
+
console.log(`\n💾 Backup created: ${backupId}`);
|
|
315
|
+
console.log(` Run 'vibecode undo' to restore previous state.\n`);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create backup manager instance
|
|
322
|
+
*/
|
|
323
|
+
export function createBackupManager(projectPath) {
|
|
324
|
+
return new BackupManager(projectPath);
|
|
325
|
+
}
|