undoai 0.1.0-beta.2 ā 1.0.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/CONTRIBUTING.md +287 -0
- package/QUICKSTART.md +90 -0
- package/USAGE.md +474 -0
- package/demo-feature/README.md +1 -0
- package/demo-feature/config.ts +1 -0
- package/demo-feature/index.ts +1 -0
- package/demo-feature/types.ts +1 -0
- package/demo-feature/user-service.ts +1 -0
- package/dist/cli/index.js +0 -0
- package/fitur_premium.md +5 -0
- package/package.json +7 -28
- package/run-all-tests.sh +30 -0
- package/src/cli/commands/restore.ts +222 -0
- package/src/cli/commands/status.ts +51 -0
- package/src/cli/commands/stop.ts +22 -0
- package/src/cli/commands/watch.ts +162 -0
- package/src/cli/index.ts +52 -0
- package/src/core/daemon.ts +104 -0
- package/src/core/snapshot.ts +144 -0
- package/src/core/storage.ts +250 -0
- package/src/example.ts +33 -0
- package/src/utils/logger.ts +69 -0
- package/src/watcher.ts +163 -0
- package/test-1-burst.sh +36 -0
- package/test-2-selective.sh +27 -0
- package/test-3-compression.sh +95 -0
- package/test-burst-1.ts +1 -0
- package/test-burst-2.ts +1 -0
- package/test-burst-3.ts +1 -0
- package/test-compression/file1.txt +2 -0
- package/test-compression/file2.txt +2 -0
- package/test-compression/file3.txt +1 -0
- package/test-watcher.sh +21 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { SnapshotManager } from '../../core/snapshot.js';
|
|
3
|
+
import { Storage } from '../../core/storage.js';
|
|
4
|
+
import { Logger } from '../../utils/logger.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { minimatch } from 'minimatch';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format relative time
|
|
10
|
+
*/
|
|
11
|
+
function formatRelativeTime(timestamp: number): string {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const diff = now - timestamp;
|
|
14
|
+
|
|
15
|
+
const seconds = Math.floor(diff / 1000);
|
|
16
|
+
const minutes = Math.floor(seconds / 60);
|
|
17
|
+
const hours = Math.floor(minutes / 60);
|
|
18
|
+
const days = Math.floor(hours / 24);
|
|
19
|
+
|
|
20
|
+
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
21
|
+
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
22
|
+
if (minutes > 0) return `${minutes} min${minutes > 1 ? 's' : ''} ago`;
|
|
23
|
+
return 'just now';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RestoreOptions {
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
files?: string;
|
|
29
|
+
pattern?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Restore command - restore files from a snapshot
|
|
34
|
+
*/
|
|
35
|
+
export async function restoreCommand(options: RestoreOptions = {}): Promise<void> {
|
|
36
|
+
// Initialize storage if needed
|
|
37
|
+
if (!Storage.isInitialized()) {
|
|
38
|
+
Logger.error('No snapshots found. Start watching with "undoai watch" first.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get current directory
|
|
43
|
+
const projectRoot = process.cwd();
|
|
44
|
+
const snapshotManager = new SnapshotManager(projectRoot);
|
|
45
|
+
|
|
46
|
+
// List available snapshots
|
|
47
|
+
const snapshots = snapshotManager.listSnapshots();
|
|
48
|
+
|
|
49
|
+
// Filter out empty snapshots (snapshots with 0 files)
|
|
50
|
+
const validSnapshots = snapshots.filter(s => s.metadata.fileCount > 0);
|
|
51
|
+
|
|
52
|
+
if (validSnapshots.length === 0) {
|
|
53
|
+
Logger.error('No snapshots available');
|
|
54
|
+
Logger.info('Start watching with "undoai watch" to create snapshots');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('');
|
|
59
|
+
Logger.info('Available snapshots:');
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
// Create choices for inquirer
|
|
63
|
+
const choices = validSnapshots.map((snapshot: { id: string; metadata: any }, index: number) => {
|
|
64
|
+
const { id, metadata } = snapshot;
|
|
65
|
+
const relativeTime = formatRelativeTime(metadata.timestamp);
|
|
66
|
+
const label = metadata.label === 'AI_BURST' ? 'š¤ AI' : 'š Auto';
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
name: `${index + 1}. [${relativeTime}] ${metadata.fileCount} file${metadata.fileCount > 1 ? 's' : ''} ${label}`,
|
|
70
|
+
value: id,
|
|
71
|
+
short: `Snapshot ${index + 1}`,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Add cancel option
|
|
76
|
+
choices.push({
|
|
77
|
+
name: 'ā Cancel',
|
|
78
|
+
value: 'CANCEL',
|
|
79
|
+
short: 'Cancel',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Prompt user to select snapshot
|
|
83
|
+
const { snapshotId } = await inquirer.prompt([
|
|
84
|
+
{
|
|
85
|
+
type: 'list',
|
|
86
|
+
name: 'snapshotId',
|
|
87
|
+
message: 'Which snapshot do you want to restore?',
|
|
88
|
+
choices,
|
|
89
|
+
pageSize: 10,
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
if (snapshotId === 'CANCEL') {
|
|
94
|
+
Logger.info('Restore cancelled');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get snapshot details
|
|
99
|
+
const snapshot = snapshotManager.getSnapshot(snapshotId);
|
|
100
|
+
if (!snapshot) {
|
|
101
|
+
Logger.error('Snapshot not found');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Determine which files to restore
|
|
106
|
+
let filesToRestore = snapshot.metadata.changedFiles;
|
|
107
|
+
|
|
108
|
+
// FEATURE: Pattern-based filtering
|
|
109
|
+
if (options.files) {
|
|
110
|
+
const fileList = options.files.split(',').map(f => f.trim());
|
|
111
|
+
filesToRestore = filesToRestore.filter((file: string) =>
|
|
112
|
+
fileList.some(pattern => minimatch(file, pattern))
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (filesToRestore.length === 0) {
|
|
116
|
+
Logger.error('No files match the specified pattern');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('');
|
|
121
|
+
Logger.info(`Matched ${filesToRestore.length} files from pattern`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// FEATURE: Interactive file selection
|
|
125
|
+
if (options.interactive) {
|
|
126
|
+
console.log('');
|
|
127
|
+
Logger.info('Select files to restore:');
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
const fileChoices = filesToRestore.map((file: string) => {
|
|
131
|
+
const relativePath = path.relative(projectRoot, file);
|
|
132
|
+
return {
|
|
133
|
+
name: relativePath,
|
|
134
|
+
value: file,
|
|
135
|
+
checked: false, // Default unchecked
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const { selectedFiles } = await inquirer.prompt([
|
|
140
|
+
{
|
|
141
|
+
type: 'checkbox',
|
|
142
|
+
name: 'selectedFiles',
|
|
143
|
+
message: 'Select files (Space to select, Enter to confirm):',
|
|
144
|
+
choices: fileChoices,
|
|
145
|
+
pageSize: 15,
|
|
146
|
+
validate: (answer) => {
|
|
147
|
+
if (answer.length === 0) {
|
|
148
|
+
return 'You must select at least one file';
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
filesToRestore = selectedFiles;
|
|
156
|
+
|
|
157
|
+
if (filesToRestore.length === 0) {
|
|
158
|
+
Logger.info('No files selected');
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Show preview of what will be restored
|
|
164
|
+
console.log('');
|
|
165
|
+
Logger.info(`š Preview: ${filesToRestore.length} file${filesToRestore.length > 1 ? 's' : ''} will be restored`);
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
filesToRestore.slice(0, 10).forEach((file: string) => {
|
|
169
|
+
const relativePath = path.relative(projectRoot, file);
|
|
170
|
+
console.log(` āļø ${relativePath}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (filesToRestore.length > 10) {
|
|
174
|
+
console.log(` ... and ${filesToRestore.length - 10} more`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('');
|
|
178
|
+
Logger.warn('ā ļø Current changes to these files will be overwritten');
|
|
179
|
+
|
|
180
|
+
// Confirm
|
|
181
|
+
const { confirm } = await inquirer.prompt([
|
|
182
|
+
{
|
|
183
|
+
type: 'confirm',
|
|
184
|
+
name: 'confirm',
|
|
185
|
+
message: 'Continue with restore?',
|
|
186
|
+
default: true,
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
if (!confirm) {
|
|
191
|
+
Logger.info('Restore cancelled');
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Restore snapshot (selective or full)
|
|
196
|
+
try {
|
|
197
|
+
let restoredCount = 0;
|
|
198
|
+
|
|
199
|
+
if (options.interactive || options.files) {
|
|
200
|
+
// Selective restore
|
|
201
|
+
for (const file of filesToRestore) {
|
|
202
|
+
try {
|
|
203
|
+
Storage.restoreFileFromSnapshot(file, snapshotId, projectRoot);
|
|
204
|
+
restoredCount++;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
Logger.warn(`Failed to restore ${file}: ${error}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// Full restore
|
|
211
|
+
restoredCount = snapshotManager.restoreSnapshot(snapshotId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
console.log('');
|
|
215
|
+
Logger.success(`ā
Restored ${restoredCount} file${restoredCount > 1 ? 's' : ''}`);
|
|
216
|
+
Logger.dim(`From: ${formatRelativeTime(snapshot.metadata.timestamp)}`);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.log('');
|
|
219
|
+
Logger.error(`Failed to restore: ${error}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DaemonManager } from '../../core/daemon.js';
|
|
2
|
+
import { Storage, STORAGE_PATHS } from '../../core/storage.js';
|
|
3
|
+
import { Logger } from '../../utils/logger.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Status command - show daemon status and info
|
|
8
|
+
*/
|
|
9
|
+
export async function statusCommand(): Promise<void> {
|
|
10
|
+
const isRunning = DaemonManager.isRunning();
|
|
11
|
+
const isInitialized = Storage.isInitialized();
|
|
12
|
+
|
|
13
|
+
console.log('');
|
|
14
|
+
Logger.bold('undoai Status');
|
|
15
|
+
console.log('');
|
|
16
|
+
|
|
17
|
+
// Daemon status
|
|
18
|
+
if (isRunning) {
|
|
19
|
+
const pid = DaemonManager.getPid();
|
|
20
|
+
console.log(chalk.green('š¢ Running') + chalk.dim(` (PID: ${pid})`));
|
|
21
|
+
} else {
|
|
22
|
+
console.log(chalk.red('š“ Not running'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('');
|
|
26
|
+
|
|
27
|
+
// Storage info
|
|
28
|
+
if (isInitialized) {
|
|
29
|
+
const snapshotIds = Storage.getSnapshotIds();
|
|
30
|
+
const snapshotCount = snapshotIds.length;
|
|
31
|
+
const storageSize = Storage.getStorageSize();
|
|
32
|
+
|
|
33
|
+
Logger.dim('š¾ Storage:');
|
|
34
|
+
console.log(chalk.dim(` Location: ${STORAGE_PATHS.root}`));
|
|
35
|
+
console.log(chalk.dim(` Snapshots: ${snapshotCount}`));
|
|
36
|
+
console.log(chalk.dim(` Size: ${Storage.formatBytes(storageSize)}`));
|
|
37
|
+
} else {
|
|
38
|
+
Logger.dim('š¾ Storage: Not initialized');
|
|
39
|
+
Logger.dim(' Run "undoai watch" to initialize');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
// Show commands
|
|
45
|
+
Logger.dim('š Commands:');
|
|
46
|
+
console.log(chalk.dim(' undoai watch - Start watching'));
|
|
47
|
+
console.log(chalk.dim(' undoai restore - Restore snapshot'));
|
|
48
|
+
console.log(chalk.dim(' undoai stop - Stop watching'));
|
|
49
|
+
console.log(chalk.dim(' undoai status - Show this status'));
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DaemonManager } from '../../core/daemon.js';
|
|
2
|
+
import { Logger } from '../../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stop command - stop watching daemon
|
|
6
|
+
*/
|
|
7
|
+
export async function stopCommand(): Promise<void> {
|
|
8
|
+
if (!DaemonManager.isRunning()) {
|
|
9
|
+
Logger.error('undoai is not running');
|
|
10
|
+
Logger.info('Use "undoai watch" to start watching');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const stopped = DaemonManager.stop();
|
|
15
|
+
|
|
16
|
+
if (stopped) {
|
|
17
|
+
Logger.success('undoai stopped');
|
|
18
|
+
} else {
|
|
19
|
+
Logger.error('Failed to stop undoai');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { FileWatcher } from '../../watcher.js';
|
|
2
|
+
import { SnapshotManager } from '../../core/snapshot.js';
|
|
3
|
+
import { Storage, STORAGE_PATHS } from '../../core/storage.js';
|
|
4
|
+
import { DaemonManager } from '../../core/daemon.js';
|
|
5
|
+
import { Logger } from '../../utils/logger.js';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { minimatch } from 'minimatch';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Important file patterns that should trigger snapshot even if only 1 file changed
|
|
11
|
+
*/
|
|
12
|
+
const IMPORTANT_FILE_PATTERNS = [
|
|
13
|
+
'.env',
|
|
14
|
+
'.env.*',
|
|
15
|
+
'package.json',
|
|
16
|
+
'package-lock.json',
|
|
17
|
+
'pnpm-lock.yaml',
|
|
18
|
+
'yarn.lock',
|
|
19
|
+
'tsconfig.json',
|
|
20
|
+
'jsconfig.json',
|
|
21
|
+
'**/*.prisma',
|
|
22
|
+
'**/schema.prisma',
|
|
23
|
+
'**/migrations/**',
|
|
24
|
+
'Dockerfile',
|
|
25
|
+
'docker-compose.yml',
|
|
26
|
+
'docker-compose.*.yml',
|
|
27
|
+
'.github/workflows/**',
|
|
28
|
+
'.gitlab-ci.yml',
|
|
29
|
+
'Jenkinsfile',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if file is important (should trigger snapshot even if single file)
|
|
34
|
+
*/
|
|
35
|
+
function isImportantFile(filePath: string, projectRoot: string): boolean {
|
|
36
|
+
const relativePath = path.relative(projectRoot, filePath);
|
|
37
|
+
return IMPORTANT_FILE_PATTERNS.some(pattern => minimatch(relativePath, pattern));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Smart detection: should we create snapshot?
|
|
42
|
+
*/
|
|
43
|
+
function shouldCreateSnapshot(
|
|
44
|
+
changedFiles: Set<string>,
|
|
45
|
+
projectRoot: string,
|
|
46
|
+
timeDiff?: number
|
|
47
|
+
): { should: boolean; reason: string } {
|
|
48
|
+
const fileCount = changedFiles.size;
|
|
49
|
+
|
|
50
|
+
// Priority 1: Burst detection (ā„3 files)
|
|
51
|
+
if (fileCount >= 3) {
|
|
52
|
+
return { should: true, reason: `Burst detected (${fileCount} files)` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Priority 2: Important file changed (even if 1 file)
|
|
56
|
+
const hasImportant = Array.from(changedFiles).some(file =>
|
|
57
|
+
isImportantFile(file, projectRoot)
|
|
58
|
+
);
|
|
59
|
+
if (hasImportant) {
|
|
60
|
+
return { should: true, reason: 'Important file changed' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Priority 3: Velocity-based AI detection
|
|
64
|
+
if (timeDiff && fileCount >= 2 && timeDiff < 1000) {
|
|
65
|
+
// 2+ files in < 1 second = likely AI
|
|
66
|
+
return { should: true, reason: 'High velocity change (likely AI)' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { should: false, reason: 'Below threshold' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Watch command - start watching for file changes
|
|
74
|
+
*/
|
|
75
|
+
export async function watchCommand(): Promise<void> {
|
|
76
|
+
// Check if already running
|
|
77
|
+
if (DaemonManager.isRunning()) {
|
|
78
|
+
Logger.error('undoai is already watching');
|
|
79
|
+
Logger.info('Use "undoai stop" to stop watching');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Initialize storage
|
|
84
|
+
Storage.init();
|
|
85
|
+
|
|
86
|
+
// Get current directory as project root
|
|
87
|
+
const projectRoot = process.cwd();
|
|
88
|
+
|
|
89
|
+
// Create snapshot manager
|
|
90
|
+
const snapshotManager = new SnapshotManager(projectRoot);
|
|
91
|
+
|
|
92
|
+
// Create initial snapshot if needed
|
|
93
|
+
const existingSnapshots = snapshotManager.listSnapshots();
|
|
94
|
+
if (existingSnapshots.length === 0) {
|
|
95
|
+
console.log('');
|
|
96
|
+
Logger.info('šø Creating initial baseline snapshot...');
|
|
97
|
+
Logger.dim('This captures your current working state');
|
|
98
|
+
|
|
99
|
+
// We'll implement full initial snapshot in next iteration
|
|
100
|
+
// For now, just log that baseline will be created on first change
|
|
101
|
+
Logger.info('š” Baseline will be created on first file change');
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let lastChangeTime = Date.now();
|
|
106
|
+
|
|
107
|
+
// Create file watcher with smart detection
|
|
108
|
+
const watcher = new FileWatcher({
|
|
109
|
+
watchPath: projectRoot,
|
|
110
|
+
onBurstChange: (changedFiles: Set<string>) => {
|
|
111
|
+
const currentTime = Date.now();
|
|
112
|
+
const timeDiff = currentTime - lastChangeTime;
|
|
113
|
+
lastChangeTime = currentTime;
|
|
114
|
+
|
|
115
|
+
// Smart detection
|
|
116
|
+
const detection = shouldCreateSnapshot(changedFiles, projectRoot, timeDiff);
|
|
117
|
+
|
|
118
|
+
if (detection.should) {
|
|
119
|
+
// Create snapshot
|
|
120
|
+
try {
|
|
121
|
+
const snapshotId = snapshotManager.createSnapshot(changedFiles, 'AI_BURST');
|
|
122
|
+
Logger.snapshot(`Snapshot saved (${changedFiles.size} files changed)`);
|
|
123
|
+
Logger.dim(`Reason: ${detection.reason}`);
|
|
124
|
+
Logger.dim(`Snapshot ID: ${snapshotId}`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
Logger.error(`Failed to create snapshot: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// Just log, no snapshot
|
|
130
|
+
Logger.dim(`${changedFiles.size} file(s) changed (${detection.reason})`);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
burstThreshold: 3, // Lowered from 5 to 3
|
|
134
|
+
debounceDelay: 2000,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Start watching
|
|
138
|
+
watcher.start();
|
|
139
|
+
|
|
140
|
+
// Mark daemon as running
|
|
141
|
+
DaemonManager.markAsRunning(process.pid);
|
|
142
|
+
|
|
143
|
+
// Display startup message
|
|
144
|
+
console.log('');
|
|
145
|
+
Logger.success('undoai is now watching');
|
|
146
|
+
Logger.dim(`š Project: ${projectRoot}`);
|
|
147
|
+
Logger.dim(`š¾ Storage: ${STORAGE_PATHS.root}`);
|
|
148
|
+
Logger.dim(`š 100% local - your code never leaves this machine`);
|
|
149
|
+
console.log('');
|
|
150
|
+
Logger.info('šÆ Smart detection enabled:');
|
|
151
|
+
Logger.dim(' ⢠ā„3 files changed = snapshot');
|
|
152
|
+
Logger.dim(' ⢠Important files (.env, package.json, etc) = snapshot');
|
|
153
|
+
Logger.dim(' ⢠High velocity changes = snapshot');
|
|
154
|
+
console.log('');
|
|
155
|
+
Logger.info('Watching for file changes... (Press Ctrl+C to stop)');
|
|
156
|
+
console.log('');
|
|
157
|
+
|
|
158
|
+
// Setup graceful shutdown
|
|
159
|
+
DaemonManager.setupShutdownHandlers(async () => {
|
|
160
|
+
await watcher.stop();
|
|
161
|
+
});
|
|
162
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { watchCommand } from './commands/watch.js';
|
|
5
|
+
import { restoreCommand } from './commands/restore.js';
|
|
6
|
+
import { stopCommand } from './commands/stop.js';
|
|
7
|
+
import { statusCommand } from './commands/status.js';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('undoai')
|
|
13
|
+
.description('Free, local undo button for AI coding')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
// Watch command
|
|
17
|
+
program
|
|
18
|
+
.command('watch')
|
|
19
|
+
.description('Start watching for file changes')
|
|
20
|
+
.action(async () => {
|
|
21
|
+
await watchCommand();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Restore command
|
|
25
|
+
program
|
|
26
|
+
.command('restore')
|
|
27
|
+
.description('Restore files from a snapshot')
|
|
28
|
+
.option('-i, --interactive', 'Select specific files to restore')
|
|
29
|
+
.option('-f, --files <patterns>', 'Restore specific files (comma-separated patterns)')
|
|
30
|
+
.option('-p, --pattern <glob>', 'Restore files matching glob pattern')
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
await restoreCommand(options);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Stop command
|
|
36
|
+
program
|
|
37
|
+
.command('stop')
|
|
38
|
+
.description('Stop watching daemon')
|
|
39
|
+
.action(async () => {
|
|
40
|
+
await stopCommand();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Status command
|
|
44
|
+
program
|
|
45
|
+
.command('status')
|
|
46
|
+
.description('Show undoai status')
|
|
47
|
+
.action(async () => {
|
|
48
|
+
await statusCommand();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Parse arguments
|
|
52
|
+
program.parse();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
3
|
+
import { STORAGE_PATHS } from './storage.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Daemon process manager
|
|
7
|
+
*/
|
|
8
|
+
export class DaemonManager {
|
|
9
|
+
/**
|
|
10
|
+
* Check if daemon is running
|
|
11
|
+
*/
|
|
12
|
+
static isRunning(): boolean {
|
|
13
|
+
if (!fs.existsSync(STORAGE_PATHS.daemonPid)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const pid = parseInt(fs.readFileSync(STORAGE_PATHS.daemonPid, 'utf-8'));
|
|
19
|
+
|
|
20
|
+
// Check if process exists
|
|
21
|
+
process.kill(pid, 0);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// Process doesn't exist, clean up stale PID file
|
|
25
|
+
this.cleanupPidFile();
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get daemon PID
|
|
32
|
+
*/
|
|
33
|
+
static getPid(): number | null {
|
|
34
|
+
if (!fs.existsSync(STORAGE_PATHS.daemonPid)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return parseInt(fs.readFileSync(STORAGE_PATHS.daemonPid, 'utf-8'));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save daemon PID
|
|
47
|
+
*/
|
|
48
|
+
static savePid(pid: number): void {
|
|
49
|
+
fs.writeFileSync(STORAGE_PATHS.daemonPid, pid.toString(), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clean up PID file
|
|
54
|
+
*/
|
|
55
|
+
static cleanupPidFile(): void {
|
|
56
|
+
if (fs.existsSync(STORAGE_PATHS.daemonPid)) {
|
|
57
|
+
fs.unlinkSync(STORAGE_PATHS.daemonPid);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stop daemon process
|
|
63
|
+
*/
|
|
64
|
+
static stop(): boolean {
|
|
65
|
+
const pid = this.getPid();
|
|
66
|
+
|
|
67
|
+
if (!pid) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 'SIGTERM');
|
|
73
|
+
this.cleanupPidFile();
|
|
74
|
+
return true;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
// Process doesn't exist
|
|
77
|
+
this.cleanupPidFile();
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start daemon in background (for watch command)
|
|
84
|
+
* Note: This is called from the watch command itself
|
|
85
|
+
*/
|
|
86
|
+
static markAsRunning(pid: number): void {
|
|
87
|
+
this.savePid(pid);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Setup graceful shutdown handlers
|
|
92
|
+
*/
|
|
93
|
+
static setupShutdownHandlers(onShutdown: () => Promise<void>): void {
|
|
94
|
+
const shutdown = async (signal: string) => {
|
|
95
|
+
console.log(`\n\nš Received ${signal}, stopping watcher...`);
|
|
96
|
+
await onShutdown();
|
|
97
|
+
this.cleanupPidFile();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
102
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
103
|
+
}
|
|
104
|
+
}
|