vibefast-cli 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTO-DETECT-DEPS.md +607 -0
- package/CHANGELOG.md +86 -0
- package/FINAL-PACKAGE-STRATEGY.md +583 -0
- package/FINAL-SIMPLE-PLAN.md +487 -0
- package/FLOW-DIAGRAM.md +1629 -0
- package/GOTCHAS-AND-RISKS.md +801 -0
- package/IMPLEMENTATION-COMPLETE.md +477 -0
- package/IMPLEMENTATION-PLAN.md +1360 -0
- package/MONITORING-AND-ANNOUNCEMENT-GUIDE.md +669 -0
- package/PRE-PUBLISH-CHECKLIST.md +558 -0
- package/PRODUCTION-READINESS.md +684 -0
- package/PRODUCTION-TEST-RESULTS.md +465 -0
- package/PUBLISHED-SUCCESS.md +282 -0
- package/README.md +73 -7
- package/READY-TO-PUBLISH.md +419 -0
- package/SIMPLIFIED-PLAN.md +578 -0
- package/TEST-SUMMARY.md +261 -0
- package/USER-MODIFICATIONS.md +448 -0
- package/cloudflare-worker/worker.js +26 -6
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +192 -15
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/checklist.d.ts +3 -0
- package/dist/commands/checklist.d.ts.map +1 -0
- package/dist/commands/checklist.js +64 -0
- package/dist/commands/checklist.js.map +1 -0
- package/dist/commands/devices.d.ts.map +1 -1
- package/dist/commands/devices.js +27 -1
- package/dist/commands/devices.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +38 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +85 -2
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +40 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/core/__tests__/fsx.test.d.ts +2 -0
- package/dist/core/__tests__/fsx.test.d.ts.map +1 -0
- package/dist/core/__tests__/fsx.test.js +79 -0
- package/dist/core/__tests__/fsx.test.js.map +1 -0
- package/dist/core/__tests__/hash.test.d.ts +2 -0
- package/dist/core/__tests__/hash.test.d.ts.map +1 -0
- package/dist/core/__tests__/hash.test.js +84 -0
- package/dist/core/__tests__/hash.test.js.map +1 -0
- package/dist/core/__tests__/journal.test.js +65 -0
- package/dist/core/__tests__/journal.test.js.map +1 -1
- package/dist/core/__tests__/prompt.test.d.ts +2 -0
- package/dist/core/__tests__/prompt.test.d.ts.map +1 -0
- package/dist/core/__tests__/prompt.test.js +56 -0
- package/dist/core/__tests__/prompt.test.js.map +1 -0
- package/dist/core/fsx.d.ts +7 -1
- package/dist/core/fsx.d.ts.map +1 -1
- package/dist/core/fsx.js +18 -3
- package/dist/core/fsx.js.map +1 -1
- package/dist/core/hash.d.ts +13 -0
- package/dist/core/hash.d.ts.map +1 -0
- package/dist/core/hash.js +69 -0
- package/dist/core/hash.js.map +1 -0
- package/dist/core/journal.d.ts +10 -1
- package/dist/core/journal.d.ts.map +1 -1
- package/dist/core/journal.js +23 -1
- package/dist/core/journal.js.map +1 -1
- package/dist/core/prompt.d.ts +11 -0
- package/dist/core/prompt.d.ts.map +1 -0
- package/dist/core/prompt.js +34 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/commands/add.ts +234 -16
- package/src/commands/checklist.ts +71 -0
- package/src/commands/devices.ts +28 -1
- package/src/commands/list.ts +39 -1
- package/src/commands/remove.ts +105 -3
- package/src/commands/status.ts +47 -0
- package/src/core/__tests__/fsx.test.ts +101 -0
- package/src/core/__tests__/hash.test.ts +112 -0
- package/src/core/__tests__/journal.test.ts +76 -0
- package/src/core/__tests__/prompt.test.ts +72 -0
- package/src/core/fsx.ts +38 -5
- package/src/core/hash.ts +84 -0
- package/src/core/journal.ts +40 -2
- package/src/core/prompt.ts +40 -0
- package/src/index.ts +4 -0
- package/text.md +27 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../core/log.js';
|
|
3
|
+
import { getPaths } from '../core/paths.js';
|
|
4
|
+
import { getEntry } from '../core/journal.js';
|
|
5
|
+
|
|
6
|
+
export const checklistCommand = new Command('checklist')
|
|
7
|
+
.description('Show manual setup steps for an installed feature')
|
|
8
|
+
.argument('<feature>', 'Feature name')
|
|
9
|
+
.option('--target <target>', 'Target platform (native or web)', 'native')
|
|
10
|
+
.action(async (feature: string, options) => {
|
|
11
|
+
try {
|
|
12
|
+
const paths = getPaths();
|
|
13
|
+
const target = options.target as 'native' | 'web';
|
|
14
|
+
|
|
15
|
+
// Check if feature is installed
|
|
16
|
+
const entry = await getEntry(paths.journalFile, feature, target);
|
|
17
|
+
if (!entry) {
|
|
18
|
+
log.error(`${feature} is not installed for ${target}`);
|
|
19
|
+
log.info('Run "vf status" to see installed features');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if feature has manual steps
|
|
24
|
+
if (!entry.manifest?.manualSteps || entry.manifest.manualSteps.length === 0) {
|
|
25
|
+
log.info(`${feature} has no manual setup steps`);
|
|
26
|
+
log.success('This feature is ready to use!');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Display manual steps
|
|
31
|
+
log.info(`Manual setup steps for ${feature}:`);
|
|
32
|
+
log.plain('');
|
|
33
|
+
|
|
34
|
+
entry.manifest.manualSteps.forEach((step: any, index: number) => {
|
|
35
|
+
log.plain(`Step ${index + 1}: ${step.title}`);
|
|
36
|
+
log.plain(` ${step.description}`);
|
|
37
|
+
if (step.link) {
|
|
38
|
+
log.plain(` 🔗 ${step.link}`);
|
|
39
|
+
}
|
|
40
|
+
if (step.file) {
|
|
41
|
+
log.plain(` 📝 File: ${step.file}`);
|
|
42
|
+
}
|
|
43
|
+
if (step.content) {
|
|
44
|
+
log.plain(` Add: ${step.content}`);
|
|
45
|
+
}
|
|
46
|
+
log.plain('');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Display environment variables if any
|
|
50
|
+
if (entry.manifest.env && entry.manifest.env.length > 0) {
|
|
51
|
+
log.warn('⚠ REQUIRED ENVIRONMENT VARIABLES:');
|
|
52
|
+
log.plain('');
|
|
53
|
+
|
|
54
|
+
entry.manifest.env.forEach((envVar: any) => {
|
|
55
|
+
log.plain(` ${envVar.key}`);
|
|
56
|
+
log.plain(` ${envVar.description}`);
|
|
57
|
+
log.plain(` Example: ${envVar.example}`);
|
|
58
|
+
if (envVar.link) {
|
|
59
|
+
log.plain(` Get it: ${envVar.link}`);
|
|
60
|
+
}
|
|
61
|
+
log.plain('');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
log.info('Add these to your .env file');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} catch (error: any) {
|
|
68
|
+
log.error(`Failed to show checklist: ${error.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
});
|
package/src/commands/devices.ts
CHANGED
|
@@ -32,7 +32,34 @@ export const devicesCommand = new Command('devices')
|
|
|
32
32
|
log.plain(` • ${device.id} (${device.os}/${device.arch}) - ${device.lastSeen}`);
|
|
33
33
|
});
|
|
34
34
|
} catch (error: any) {
|
|
35
|
-
|
|
35
|
+
const errorMsg = error.message || '';
|
|
36
|
+
|
|
37
|
+
log.plain('');
|
|
38
|
+
log.error('Failed to manage devices');
|
|
39
|
+
log.plain('');
|
|
40
|
+
|
|
41
|
+
// Better error messages
|
|
42
|
+
if (errorMsg.includes('license_key not found') || errorMsg.includes('Invalid') || errorMsg.includes('token')) {
|
|
43
|
+
log.plain('❌ Invalid or expired license key');
|
|
44
|
+
log.plain('');
|
|
45
|
+
log.info('To fix this:');
|
|
46
|
+
log.plain(' 1. Run: vf logout');
|
|
47
|
+
log.plain(' 2. Run: vf login --token YOUR_LICENSE_KEY');
|
|
48
|
+
log.plain('');
|
|
49
|
+
log.info('Need help? Contact support@vibefast.pro');
|
|
50
|
+
} else if (errorMsg.includes('Network') || errorMsg.includes('connect') || errorMsg.includes('fetch')) {
|
|
51
|
+
log.plain('❌ Network error');
|
|
52
|
+
log.plain('');
|
|
53
|
+
log.info('Could not connect to VibeFast servers');
|
|
54
|
+
log.plain('');
|
|
55
|
+
log.plain(`Details: ${errorMsg}`);
|
|
56
|
+
} else {
|
|
57
|
+
log.plain(`❌ ${errorMsg}`);
|
|
58
|
+
log.plain('');
|
|
59
|
+
log.info('If this problem persists, contact support@vibefast.pro');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log.plain('');
|
|
36
63
|
process.exit(1);
|
|
37
64
|
}
|
|
38
65
|
});
|
package/src/commands/list.ts
CHANGED
|
@@ -39,7 +39,45 @@ export const listCommand = new Command('list')
|
|
|
39
39
|
|
|
40
40
|
log.plain('\nInstall with: vf add <feature-name>');
|
|
41
41
|
} catch (error: any) {
|
|
42
|
-
|
|
42
|
+
const errorMsg = error.message || '';
|
|
43
|
+
|
|
44
|
+
log.plain('');
|
|
45
|
+
log.error('Failed to list recipes');
|
|
46
|
+
log.plain('');
|
|
47
|
+
|
|
48
|
+
// Better error messages
|
|
49
|
+
if (errorMsg.includes('license_key not found') || errorMsg.includes('Invalid') || errorMsg.includes('token')) {
|
|
50
|
+
log.plain('❌ Invalid or expired license key');
|
|
51
|
+
log.plain('');
|
|
52
|
+
log.info('Your license key may be:');
|
|
53
|
+
log.plain(' • Incorrect or mistyped');
|
|
54
|
+
log.plain(' • Expired');
|
|
55
|
+
log.plain(' • Not yet activated');
|
|
56
|
+
log.plain('');
|
|
57
|
+
log.info('To fix this:');
|
|
58
|
+
log.plain(' 1. Check your license key from your purchase receipt');
|
|
59
|
+
log.plain(' 2. Run: vf logout');
|
|
60
|
+
log.plain(' 3. Run: vf login --token YOUR_LICENSE_KEY');
|
|
61
|
+
log.plain('');
|
|
62
|
+
log.info('Need help? Contact support@vibefast.pro');
|
|
63
|
+
} else if (errorMsg.includes('Network') || errorMsg.includes('connect') || errorMsg.includes('fetch')) {
|
|
64
|
+
log.plain('❌ Network error');
|
|
65
|
+
log.plain('');
|
|
66
|
+
log.info('Could not connect to VibeFast servers');
|
|
67
|
+
log.plain('');
|
|
68
|
+
log.info('Please check:');
|
|
69
|
+
log.plain(' • Your internet connection');
|
|
70
|
+
log.plain(' • Firewall settings');
|
|
71
|
+
log.plain(' • VPN configuration');
|
|
72
|
+
log.plain('');
|
|
73
|
+
log.plain(`Details: ${errorMsg}`);
|
|
74
|
+
} else {
|
|
75
|
+
log.plain(`❌ ${errorMsg}`);
|
|
76
|
+
log.plain('');
|
|
77
|
+
log.info('If this problem persists, contact support@vibefast.pro');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log.plain('');
|
|
43
81
|
process.exit(1);
|
|
44
82
|
}
|
|
45
83
|
});
|
package/src/commands/remove.ts
CHANGED
|
@@ -2,15 +2,19 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { log } from '../core/log.js';
|
|
3
3
|
import { getPaths } from '../core/paths.js';
|
|
4
4
|
import { validateSignature, validateTarget } from '../core/validate.js';
|
|
5
|
-
import { getEntry, removeEntry } from '../core/journal.js';
|
|
5
|
+
import { getEntry, removeEntry, FileEntry } from '../core/journal.js';
|
|
6
6
|
import { deleteFile } from '../core/fsx.js';
|
|
7
7
|
import { removeNavLinkNative, removeNavLinkWeb } from '../core/codemod.js';
|
|
8
|
+
import { hashFile } from '../core/hash.js';
|
|
9
|
+
import { promptYesNo } from '../core/prompt.js';
|
|
8
10
|
|
|
9
11
|
export const removeCommand = new Command('remove')
|
|
10
12
|
.description('Remove a VibeFast feature from your project')
|
|
11
13
|
.argument('<feature>', 'Feature name to remove')
|
|
12
14
|
.option('--target <target>', 'Target platform (native or web)', 'native')
|
|
13
15
|
.option('--dry-run', 'Preview changes without applying')
|
|
16
|
+
.option('--force', 'Skip modification check')
|
|
17
|
+
.option('--yes', 'Answer yes to all prompts')
|
|
14
18
|
.action(async (feature: string, options) => {
|
|
15
19
|
try {
|
|
16
20
|
const paths = getPaths();
|
|
@@ -35,8 +39,99 @@ export const removeCommand = new Command('remove')
|
|
|
35
39
|
|
|
36
40
|
log.info(`Removing ${feature} from ${target}...`);
|
|
37
41
|
|
|
42
|
+
// Check for modifications (unless --force)
|
|
43
|
+
if (!options.force && !options.dryRun) {
|
|
44
|
+
log.info('Checking for file modifications...');
|
|
45
|
+
|
|
46
|
+
const modifiedFiles: Array<{ path: string; status: 'modified' | 'deleted' }> = [];
|
|
47
|
+
|
|
48
|
+
// Get file paths from entry
|
|
49
|
+
const filePaths = Array.isArray(entry.files) && entry.files.length > 0
|
|
50
|
+
? typeof entry.files[0] === 'string'
|
|
51
|
+
? entry.files as string[]
|
|
52
|
+
: (entry.files as FileEntry[]).map(f => f.path)
|
|
53
|
+
: [];
|
|
54
|
+
|
|
55
|
+
const fileHashes = Array.isArray(entry.files) && entry.files.length > 0 && typeof entry.files[0] !== 'string'
|
|
56
|
+
? new Map((entry.files as FileEntry[]).map(f => [f.path, f.hash]))
|
|
57
|
+
: new Map<string, string>();
|
|
58
|
+
|
|
59
|
+
for (const filePath of filePaths) {
|
|
60
|
+
const originalHash = fileHashes.get(filePath);
|
|
61
|
+
|
|
62
|
+
// Skip if we don't have original hash (old journal format)
|
|
63
|
+
if (!originalHash) continue;
|
|
64
|
+
|
|
65
|
+
const currentHash = await hashFile(filePath);
|
|
66
|
+
|
|
67
|
+
if (currentHash === '') {
|
|
68
|
+
// File was deleted by user
|
|
69
|
+
modifiedFiles.push({ path: filePath, status: 'deleted' });
|
|
70
|
+
} else if (currentHash !== originalHash) {
|
|
71
|
+
// File was modified by user
|
|
72
|
+
modifiedFiles.push({ path: filePath, status: 'modified' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Warn about modifications
|
|
77
|
+
if (modifiedFiles.length > 0) {
|
|
78
|
+
log.plain('');
|
|
79
|
+
log.warn('⚠ WARNING: The following files were changed:');
|
|
80
|
+
log.plain('');
|
|
81
|
+
|
|
82
|
+
const modified = modifiedFiles.filter(f => f.status === 'modified');
|
|
83
|
+
const deleted = modifiedFiles.filter(f => f.status === 'deleted');
|
|
84
|
+
|
|
85
|
+
if (modified.length > 0) {
|
|
86
|
+
log.plain(' Modified by you:');
|
|
87
|
+
modified.slice(0, 10).forEach(f => {
|
|
88
|
+
const relativePath = f.path.replace(paths.cwd + '/', '');
|
|
89
|
+
log.plain(` • ${relativePath}`);
|
|
90
|
+
});
|
|
91
|
+
if (modified.length > 10) {
|
|
92
|
+
log.plain(` ... and ${modified.length - 10} more`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (deleted.length > 0) {
|
|
97
|
+
log.plain('');
|
|
98
|
+
log.plain(' Already deleted:');
|
|
99
|
+
deleted.slice(0, 5).forEach(f => {
|
|
100
|
+
const relativePath = f.path.replace(paths.cwd + '/', '');
|
|
101
|
+
log.plain(` • ${relativePath}`);
|
|
102
|
+
});
|
|
103
|
+
if (deleted.length > 5) {
|
|
104
|
+
log.plain(` ... and ${deleted.length - 5} more`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
log.plain('');
|
|
109
|
+
log.warn('⚠ Your changes will be LOST if you continue!');
|
|
110
|
+
log.info('💡 Make sure you have committed to Git.');
|
|
111
|
+
log.plain('');
|
|
112
|
+
|
|
113
|
+
if (!options.yes) {
|
|
114
|
+
const shouldContinue = promptYesNo(
|
|
115
|
+
'Continue with removal? (y/N): ',
|
|
116
|
+
false
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!shouldContinue) {
|
|
120
|
+
log.info('Removal cancelled');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
38
127
|
// Delete files
|
|
39
|
-
|
|
128
|
+
const filePaths = Array.isArray(entry.files) && entry.files.length > 0
|
|
129
|
+
? typeof entry.files[0] === 'string'
|
|
130
|
+
? entry.files as string[]
|
|
131
|
+
: (entry.files as FileEntry[]).map(f => f.path)
|
|
132
|
+
: [];
|
|
133
|
+
|
|
134
|
+
for (const file of filePaths) {
|
|
40
135
|
log.info(`Deleting ${file}`);
|
|
41
136
|
if (!options.dryRun) {
|
|
42
137
|
await deleteFile(file);
|
|
@@ -66,7 +161,14 @@ export const removeCommand = new Command('remove')
|
|
|
66
161
|
}
|
|
67
162
|
|
|
68
163
|
log.success(`${feature} removed successfully!`);
|
|
69
|
-
|
|
164
|
+
|
|
165
|
+
const fileCount = Array.isArray(entry.files) && entry.files.length > 0
|
|
166
|
+
? typeof entry.files[0] === 'string'
|
|
167
|
+
? entry.files.length
|
|
168
|
+
: (entry.files as FileEntry[]).length
|
|
169
|
+
: 0;
|
|
170
|
+
|
|
171
|
+
log.info(`Files deleted: ${fileCount}`);
|
|
70
172
|
|
|
71
173
|
if (options.dryRun) {
|
|
72
174
|
log.warn('This was a dry run. Run without --dry-run to apply changes.');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { log } from '../core/log.js';
|
|
3
|
+
import { getPaths } from '../core/paths.js';
|
|
4
|
+
import { readJournal, FileEntry } from '../core/journal.js';
|
|
5
|
+
|
|
6
|
+
export const statusCommand = new Command('status')
|
|
7
|
+
.description('Show installed features and their status')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const paths = getPaths();
|
|
11
|
+
const journal = await readJournal(paths.journalFile);
|
|
12
|
+
|
|
13
|
+
if (journal.entries.length === 0) {
|
|
14
|
+
log.info('No features installed yet');
|
|
15
|
+
log.info('Run "vf list" to see available features');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
log.info('Installed features:');
|
|
20
|
+
log.plain('');
|
|
21
|
+
|
|
22
|
+
for (const entry of journal.entries) {
|
|
23
|
+
const version = entry.manifest?.version || 'unknown';
|
|
24
|
+
const fileCount = Array.isArray(entry.files) && entry.files.length > 0
|
|
25
|
+
? typeof entry.files[0] === 'string'
|
|
26
|
+
? entry.files.length
|
|
27
|
+
: (entry.files as FileEntry[]).length
|
|
28
|
+
: 0;
|
|
29
|
+
|
|
30
|
+
log.plain(` ✓ ${entry.feature} (v${version}) - ${entry.target}`);
|
|
31
|
+
log.plain(` Files: ${fileCount}`);
|
|
32
|
+
log.plain(` Installed: ${new Date(entry.ts).toLocaleDateString()}`);
|
|
33
|
+
|
|
34
|
+
if (entry.manifest?.manualSteps && entry.manifest.manualSteps.length > 0) {
|
|
35
|
+
log.plain(` ⚠ Has manual setup steps (run: vf checklist ${entry.feature})`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log.plain('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log.info(`Total: ${journal.entries.length} feature(s) installed`);
|
|
42
|
+
|
|
43
|
+
} catch (error: any) {
|
|
44
|
+
log.error(`Failed to show status: ${error.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { copyTree, exists } from '../fsx.js';
|
|
3
|
+
import { writeFile, mkdir, rm } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
|
|
7
|
+
describe('fsx', () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
let srcDir: string;
|
|
10
|
+
let destDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
testDir = join(tmpdir(), `vibefast-test-${Date.now()}`);
|
|
14
|
+
srcDir = join(testDir, 'src');
|
|
15
|
+
destDir = join(testDir, 'dest');
|
|
16
|
+
|
|
17
|
+
await mkdir(srcDir, { recursive: true });
|
|
18
|
+
await mkdir(destDir, { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await rm(testDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('copyTree', () => {
|
|
26
|
+
it('should copy files successfully', async () => {
|
|
27
|
+
const file1 = join(srcDir, 'file1.txt');
|
|
28
|
+
const file2 = join(srcDir, 'file2.txt');
|
|
29
|
+
|
|
30
|
+
await writeFile(file1, 'content 1');
|
|
31
|
+
await writeFile(file2, 'content 2');
|
|
32
|
+
|
|
33
|
+
const result = await copyTree(srcDir, destDir);
|
|
34
|
+
|
|
35
|
+
expect(result.files).toHaveLength(2);
|
|
36
|
+
expect(result.conflicts).toHaveLength(0);
|
|
37
|
+
expect(result.skipped).toHaveLength(0);
|
|
38
|
+
|
|
39
|
+
expect(await exists(join(destDir, 'file1.txt'))).toBe(true);
|
|
40
|
+
expect(await exists(join(destDir, 'file2.txt'))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should detect conflicts', async () => {
|
|
44
|
+
const srcFile = join(srcDir, 'file.txt');
|
|
45
|
+
const destFile = join(destDir, 'file.txt');
|
|
46
|
+
|
|
47
|
+
await writeFile(srcFile, 'new content');
|
|
48
|
+
await writeFile(destFile, 'existing content');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await copyTree(srcDir, destDir);
|
|
52
|
+
expect.fail('Should have thrown error');
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
expect(error.message).toContain('File exists');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should overwrite with force flag', async () => {
|
|
59
|
+
// Create source file
|
|
60
|
+
await writeFile(join(srcDir, 'file.txt'), 'new content');
|
|
61
|
+
|
|
62
|
+
// Create existing destination file
|
|
63
|
+
await writeFile(join(destDir, 'file.txt'), 'existing content');
|
|
64
|
+
|
|
65
|
+
const result = await copyTree(srcDir, destDir, { force: true });
|
|
66
|
+
|
|
67
|
+
expect(result.files).toHaveLength(1);
|
|
68
|
+
expect(result.conflicts).toHaveLength(1);
|
|
69
|
+
expect(result.skipped).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle dry-run mode', async () => {
|
|
73
|
+
const file1 = join(srcDir, 'file1.txt');
|
|
74
|
+
await writeFile(file1, 'content');
|
|
75
|
+
|
|
76
|
+
const result = await copyTree(srcDir, destDir, { dryRun: true });
|
|
77
|
+
|
|
78
|
+
expect(result.files).toHaveLength(1);
|
|
79
|
+
expect(await exists(join(destDir, 'file1.txt'))).toBe(false); // Not actually copied
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should copy nested directories', async () => {
|
|
83
|
+
const nestedDir = join(srcDir, 'nested');
|
|
84
|
+
await mkdir(nestedDir);
|
|
85
|
+
await writeFile(join(nestedDir, 'file.txt'), 'content');
|
|
86
|
+
|
|
87
|
+
const result = await copyTree(srcDir, destDir);
|
|
88
|
+
|
|
89
|
+
expect(result.files).toHaveLength(1);
|
|
90
|
+
expect(await exists(join(destDir, 'nested', 'file.txt'))).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle empty directory', async () => {
|
|
94
|
+
const result = await copyTree(srcDir, destDir);
|
|
95
|
+
|
|
96
|
+
expect(result.files).toHaveLength(0);
|
|
97
|
+
expect(result.conflicts).toHaveLength(0);
|
|
98
|
+
expect(result.skipped).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { hashFile, hashFiles } from '../hash.js';
|
|
3
|
+
import { writeFile, unlink, mkdir, rm } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
|
|
7
|
+
describe('hash', () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
testDir = join(tmpdir(), `vibefast-test-${Date.now()}`);
|
|
12
|
+
await mkdir(testDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await rm(testDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('hashFile', () => {
|
|
20
|
+
it('should hash file content correctly', async () => {
|
|
21
|
+
const testFile = join(testDir, 'test.txt');
|
|
22
|
+
await writeFile(testFile, 'hello world');
|
|
23
|
+
|
|
24
|
+
const hash = await hashFile(testFile);
|
|
25
|
+
|
|
26
|
+
// SHA-256 of "hello world"
|
|
27
|
+
expect(hash).toBe('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return empty string for non-existent file', async () => {
|
|
31
|
+
const hash = await hashFile(join(testDir, 'does-not-exist.txt'));
|
|
32
|
+
expect(hash).toBe('');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should detect file modifications', async () => {
|
|
36
|
+
const testFile = join(testDir, 'test.txt');
|
|
37
|
+
await writeFile(testFile, 'original');
|
|
38
|
+
|
|
39
|
+
const hash1 = await hashFile(testFile);
|
|
40
|
+
|
|
41
|
+
await writeFile(testFile, 'modified');
|
|
42
|
+
|
|
43
|
+
const hash2 = await hashFile(testFile);
|
|
44
|
+
|
|
45
|
+
expect(hash1).not.toBe(hash2);
|
|
46
|
+
expect(hash1).not.toBe('');
|
|
47
|
+
expect(hash2).not.toBe('');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should produce same hash for same content', async () => {
|
|
51
|
+
const file1 = join(testDir, 'file1.txt');
|
|
52
|
+
const file2 = join(testDir, 'file2.txt');
|
|
53
|
+
|
|
54
|
+
await writeFile(file1, 'same content');
|
|
55
|
+
await writeFile(file2, 'same content');
|
|
56
|
+
|
|
57
|
+
const hash1 = await hashFile(file1);
|
|
58
|
+
const hash2 = await hashFile(file2);
|
|
59
|
+
|
|
60
|
+
expect(hash1).toBe(hash2);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('hashFiles', () => {
|
|
65
|
+
it('should hash multiple files', async () => {
|
|
66
|
+
const file1 = join(testDir, 'file1.txt');
|
|
67
|
+
const file2 = join(testDir, 'file2.txt');
|
|
68
|
+
|
|
69
|
+
await writeFile(file1, 'content 1');
|
|
70
|
+
await writeFile(file2, 'content 2');
|
|
71
|
+
|
|
72
|
+
const hashes = await hashFiles([file1, file2]);
|
|
73
|
+
|
|
74
|
+
expect(hashes.size).toBe(2);
|
|
75
|
+
expect(hashes.get(file1)).toBeTruthy();
|
|
76
|
+
expect(hashes.get(file2)).toBeTruthy();
|
|
77
|
+
expect(hashes.get(file1)).not.toBe(hashes.get(file2));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should skip binary files', async () => {
|
|
81
|
+
const textFile = join(testDir, 'file.txt');
|
|
82
|
+
const imageFile = join(testDir, 'image.png');
|
|
83
|
+
|
|
84
|
+
await writeFile(textFile, 'text content');
|
|
85
|
+
await writeFile(imageFile, 'fake image data');
|
|
86
|
+
|
|
87
|
+
const hashes = await hashFiles([textFile, imageFile]);
|
|
88
|
+
|
|
89
|
+
expect(hashes.size).toBe(2);
|
|
90
|
+
expect(hashes.get(textFile)).toBeTruthy();
|
|
91
|
+
expect(hashes.get(imageFile)).toBe(''); // Empty hash for binary
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle empty array', async () => {
|
|
95
|
+
const hashes = await hashFiles([]);
|
|
96
|
+
expect(hashes.size).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle non-existent files gracefully', async () => {
|
|
100
|
+
const existingFile = join(testDir, 'exists.txt');
|
|
101
|
+
const missingFile = join(testDir, 'missing.txt');
|
|
102
|
+
|
|
103
|
+
await writeFile(existingFile, 'content');
|
|
104
|
+
|
|
105
|
+
const hashes = await hashFiles([existingFile, missingFile]);
|
|
106
|
+
|
|
107
|
+
expect(hashes.size).toBe(1);
|
|
108
|
+
expect(hashes.get(existingFile)).toBeTruthy();
|
|
109
|
+
expect(hashes.has(missingFile)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -116,4 +116,80 @@ describe('journal', () => {
|
|
|
116
116
|
expect(found).not.toBeNull();
|
|
117
117
|
expect(found?.feature).toBe('test');
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
it('should handle new format with file hashes', async () => {
|
|
121
|
+
const entry = {
|
|
122
|
+
feature: 'charts',
|
|
123
|
+
target: 'native' as const,
|
|
124
|
+
files: [
|
|
125
|
+
{ path: '/file1.ts', hash: 'abc123' },
|
|
126
|
+
{ path: '/file2.ts', hash: 'def456' },
|
|
127
|
+
],
|
|
128
|
+
insertedNav: true,
|
|
129
|
+
ts: Date.now(),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await addEntry(journalPath, entry);
|
|
133
|
+
const journal = await readJournal(journalPath);
|
|
134
|
+
|
|
135
|
+
expect(journal.entries).toHaveLength(1);
|
|
136
|
+
expect(journal.entries[0].files).toHaveLength(2);
|
|
137
|
+
expect(journal.entries[0].files[0]).toHaveProperty('path');
|
|
138
|
+
expect(journal.entries[0].files[0]).toHaveProperty('hash');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should migrate old format to new format', async () => {
|
|
142
|
+
// Write old format manually
|
|
143
|
+
const oldJournal = {
|
|
144
|
+
entries: [
|
|
145
|
+
{
|
|
146
|
+
feature: 'old-feature',
|
|
147
|
+
target: 'native' as const,
|
|
148
|
+
files: ['/file1.ts', '/file2.ts'], // Old format: array of strings
|
|
149
|
+
insertedNav: true,
|
|
150
|
+
ts: Date.now(),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await writeJournal(journalPath, oldJournal as any);
|
|
156
|
+
|
|
157
|
+
// Read should auto-migrate
|
|
158
|
+
const journal = await readJournal(journalPath);
|
|
159
|
+
|
|
160
|
+
expect(journal.entries).toHaveLength(1);
|
|
161
|
+
expect(journal.entries[0].files).toHaveLength(2);
|
|
162
|
+
|
|
163
|
+
// Should be converted to new format
|
|
164
|
+
const firstFile = journal.entries[0].files[0];
|
|
165
|
+
expect(firstFile).toHaveProperty('path');
|
|
166
|
+
expect(firstFile).toHaveProperty('hash');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should store manifest data', async () => {
|
|
170
|
+
const entry = {
|
|
171
|
+
feature: 'sentry',
|
|
172
|
+
target: 'native' as const,
|
|
173
|
+
files: [{ path: '/file.ts', hash: 'abc' }],
|
|
174
|
+
insertedNav: true,
|
|
175
|
+
ts: Date.now(),
|
|
176
|
+
manifest: {
|
|
177
|
+
version: '1.0.0',
|
|
178
|
+
manualSteps: [
|
|
179
|
+
{ title: 'Step 1', description: 'Do something' },
|
|
180
|
+
],
|
|
181
|
+
env: [
|
|
182
|
+
{ key: 'API_KEY', description: 'Your API key', example: 'xxx' },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
await addEntry(journalPath, entry);
|
|
188
|
+
const found = await getEntry(journalPath, 'sentry', 'native');
|
|
189
|
+
|
|
190
|
+
expect(found?.manifest).toBeDefined();
|
|
191
|
+
expect(found?.manifest?.version).toBe('1.0.0');
|
|
192
|
+
expect(found?.manifest?.manualSteps).toHaveLength(1);
|
|
193
|
+
expect(found?.manifest?.env).toHaveLength(1);
|
|
194
|
+
});
|
|
119
195
|
});
|