@mod-computer/cli 0.1.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.
Files changed (56) hide show
  1. package/README.md +125 -0
  2. package/commands/execute.md +156 -0
  3. package/commands/overview.md +233 -0
  4. package/commands/review.md +151 -0
  5. package/commands/spec.md +169 -0
  6. package/dist/app.js +227 -0
  7. package/dist/cli.bundle.js +25824 -0
  8. package/dist/cli.bundle.js.map +7 -0
  9. package/dist/cli.js +121 -0
  10. package/dist/commands/agents-run.js +71 -0
  11. package/dist/commands/auth.js +151 -0
  12. package/dist/commands/branch.js +1411 -0
  13. package/dist/commands/claude-sync.js +772 -0
  14. package/dist/commands/index.js +43 -0
  15. package/dist/commands/init.js +378 -0
  16. package/dist/commands/recover.js +207 -0
  17. package/dist/commands/spec.js +386 -0
  18. package/dist/commands/status.js +329 -0
  19. package/dist/commands/sync.js +95 -0
  20. package/dist/commands/workspace.js +423 -0
  21. package/dist/components/conflict-resolution-ui.js +120 -0
  22. package/dist/components/messages.js +5 -0
  23. package/dist/components/thread.js +8 -0
  24. package/dist/config/features.js +72 -0
  25. package/dist/config/release-profiles/development.json +11 -0
  26. package/dist/config/release-profiles/mvp.json +12 -0
  27. package/dist/config/release-profiles/v0.1.json +11 -0
  28. package/dist/config/release-profiles/v0.2.json +11 -0
  29. package/dist/containers/branches-container.js +140 -0
  30. package/dist/containers/directory-container.js +92 -0
  31. package/dist/containers/thread-container.js +214 -0
  32. package/dist/containers/threads-container.js +27 -0
  33. package/dist/containers/workspaces-container.js +27 -0
  34. package/dist/daemon-worker.js +257 -0
  35. package/dist/lib/auth-server.js +153 -0
  36. package/dist/lib/browser.js +35 -0
  37. package/dist/lib/storage.js +203 -0
  38. package/dist/services/automatic-file-tracker.js +303 -0
  39. package/dist/services/cli-orchestrator.js +227 -0
  40. package/dist/services/feature-flags.js +187 -0
  41. package/dist/services/file-import-service.js +283 -0
  42. package/dist/services/file-transformation-service.js +218 -0
  43. package/dist/services/logger.js +44 -0
  44. package/dist/services/mod-config.js +61 -0
  45. package/dist/services/modignore-service.js +326 -0
  46. package/dist/services/sync-daemon.js +244 -0
  47. package/dist/services/thread-notification-service.js +50 -0
  48. package/dist/services/thread-service.js +147 -0
  49. package/dist/stores/use-directory-store.js +96 -0
  50. package/dist/stores/use-threads-store.js +46 -0
  51. package/dist/stores/use-workspaces-store.js +32 -0
  52. package/dist/types/config.js +16 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/workspace-connection.js +2 -0
  55. package/dist/types.js +1 -0
  56. package/package.json +67 -0
@@ -0,0 +1,44 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ // Evaluate debug flag at call time to allow dotenv to load later
4
+ function isDebugEnabled() {
5
+ return Boolean(process.env.MOD_CLI_DEBUG) || Boolean(process.env.DEBUG);
6
+ }
7
+ const LOG_DIR = path.resolve(process.cwd(), '.mod');
8
+ const LOG_FILE = path.join(LOG_DIR, 'cli-debug.log');
9
+ function ensureLogDir() {
10
+ try {
11
+ fs.mkdirSync(LOG_DIR, { recursive: true });
12
+ }
13
+ catch { }
14
+ }
15
+ export function log(...args) {
16
+ if (!isDebugEnabled())
17
+ return;
18
+ try {
19
+ ensureLogDir();
20
+ const line = `[${new Date().toISOString()}] ${args.map(a => safeStringify(a)).join(' ')}\n`;
21
+ fs.appendFileSync(LOG_FILE, line, 'utf8');
22
+ // Also mirror to stderr so it doesn't interfere with Ink UI
23
+ try {
24
+ console.error(line.trimEnd());
25
+ }
26
+ catch { }
27
+ }
28
+ catch { }
29
+ }
30
+ function safeStringify(v) {
31
+ try {
32
+ if (typeof v === 'string')
33
+ return v;
34
+ return JSON.stringify(v);
35
+ }
36
+ catch {
37
+ try {
38
+ return String(v);
39
+ }
40
+ catch {
41
+ return '[unprintable]';
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,61 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const MOD_DIR = '.mod';
4
+ const CONFIG_FILE = 'config.json';
5
+ function getConfigPath() {
6
+ const override = process.env.MOD_CONFIG_PATH;
7
+ if (override && typeof override === 'string' && override.trim() !== '') {
8
+ return override;
9
+ }
10
+ return path.join(process.cwd(), MOD_DIR, CONFIG_FILE);
11
+ }
12
+ function ensureModDir() {
13
+ const modDirPath = path.join(process.cwd(), MOD_DIR);
14
+ if (!fs.existsSync(modDirPath)) {
15
+ fs.mkdirSync(modDirPath, { recursive: true });
16
+ }
17
+ }
18
+ export function readModConfig() {
19
+ // Preferred: .mod/config.json
20
+ const cfgPath = getConfigPath();
21
+ if (fs.existsSync(cfgPath)) {
22
+ try {
23
+ const config = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
24
+ return config || {};
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ // Legacy migration: root .mod file containing JSON
31
+ const legacyPath = path.join(process.cwd(), '.mod');
32
+ if (fs.existsSync(legacyPath) && fs.statSync(legacyPath).isFile()) {
33
+ try {
34
+ const legacy = JSON.parse(fs.readFileSync(legacyPath, 'utf8'));
35
+ const migrated = {};
36
+ if (legacy && typeof legacy.workspaceId === 'string')
37
+ migrated.workspaceId = legacy.workspaceId;
38
+ if (legacy && typeof legacy.activeBranchId === 'string')
39
+ migrated.activeBranchId = legacy.activeBranchId;
40
+ return migrated;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ export function writeModConfig(update) {
49
+ try {
50
+ ensureModDir();
51
+ const cfgPath = getConfigPath();
52
+ const current = fs.existsSync(cfgPath)
53
+ ? JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
54
+ : {};
55
+ const next = { ...current, ...update };
56
+ fs.writeFileSync(cfgPath, JSON.stringify(next, null, 2), 'utf8');
57
+ }
58
+ catch (error) {
59
+ console.error(`Failed to write .mod/config.json:`, error);
60
+ }
61
+ }
@@ -0,0 +1,326 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ export class ModIgnoreService {
4
+ constructor(workingDirectory) {
5
+ this.patterns = [];
6
+ this.defaultPatterns = [
7
+ 'node_modules/',
8
+ '*/node_modules/*',
9
+ '.git/',
10
+ '.DS_Store',
11
+ 'Thumbs.db',
12
+ '*.log',
13
+ 'npm-debug.log*',
14
+ 'yarn-debug.log*',
15
+ '.env',
16
+ '.env.*',
17
+ 'dist/',
18
+ 'build/',
19
+ '.next/',
20
+ '.nuxt/',
21
+ 'coverage/',
22
+ '.nyc_output/',
23
+ '*.tmp',
24
+ '*.temp',
25
+ '*.swp',
26
+ '*.swo',
27
+ '.cache/',
28
+ '.vscode/',
29
+ '.idea/',
30
+ '.automerge-data/',
31
+ 'package-lock.json',
32
+ 'yarn.lock',
33
+ 'pnpm-lock.yaml',
34
+ '.mod/.cache',
35
+ ];
36
+ this.trackableExtensions = [
37
+ '.md', '.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml',
38
+ '.py', '.java', '.cpp', '.c', '.h', '.css', '.scss', '.html', '.xml',
39
+ '.sql', '.go', '.rs', '.php', '.rb', '.swift', '.kt', '.scala'
40
+ ];
41
+ this.loadIgnoreFile(workingDirectory);
42
+ }
43
+ /**
44
+ * Load .modignore file from the working directory
45
+ */
46
+ loadIgnoreFile(workingDirectory) {
47
+ // Start with default patterns
48
+ this.patterns = this.defaultPatterns.map(pattern => this.parsePattern(pattern));
49
+ const ignoreFilePath = path.join(workingDirectory, '.modignore');
50
+ try {
51
+ if (fs.existsSync(ignoreFilePath)) {
52
+ const content = fs.readFileSync(ignoreFilePath, 'utf8');
53
+ const lines = content.split('\n')
54
+ .map(line => line.trim())
55
+ .filter(line => line && !line.startsWith('#')); // Remove comments and empty lines
56
+ for (const line of lines) {
57
+ this.patterns.push(this.parsePattern(line));
58
+ }
59
+ }
60
+ }
61
+ catch (err) {
62
+ console.warn('Warning: Could not read .modignore file:', err);
63
+ }
64
+ }
65
+ /**
66
+ * Parse a single ignore pattern
67
+ */
68
+ parsePattern(pattern) {
69
+ let cleanPattern = pattern.trim();
70
+ const isNegation = cleanPattern.startsWith('!');
71
+ if (isNegation) {
72
+ cleanPattern = cleanPattern.slice(1);
73
+ }
74
+ const isDirectory = cleanPattern.endsWith('/');
75
+ if (isDirectory) {
76
+ cleanPattern = cleanPattern.slice(0, -1);
77
+ }
78
+ return {
79
+ pattern: cleanPattern,
80
+ isNegation,
81
+ isDirectory,
82
+ };
83
+ }
84
+ /**
85
+ * Check if a file path should be ignored
86
+ */
87
+ shouldIgnore(filePath, workingDirectory) {
88
+ // Always ignore .mod and .modignore files
89
+ const relativePath = path.relative(workingDirectory, filePath);
90
+ const fileName = path.basename(filePath);
91
+ if (fileName === '.mod' || fileName === '.modignore') {
92
+ return true;
93
+ }
94
+ // Check if it's a directory
95
+ const isDirectory = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
96
+ let shouldIgnore = false;
97
+ for (const { pattern, isNegation, isDirectory: patternIsDirectory } of this.patterns) {
98
+ const matches = this.matchesPattern(relativePath, pattern, isDirectory, patternIsDirectory);
99
+ if (matches) {
100
+ shouldIgnore = !isNegation;
101
+ }
102
+ }
103
+ return shouldIgnore;
104
+ }
105
+ /**
106
+ * Check if a path matches a pattern
107
+ */
108
+ matchesPattern(relativePath, pattern, isDirectory, patternIsDirectory) {
109
+ // If pattern is for directories only, skip non-directories
110
+ if (patternIsDirectory && !isDirectory) {
111
+ return false;
112
+ }
113
+ // Convert pattern to regex
114
+ const regexPattern = this.patternToRegex(pattern);
115
+ const regex = new RegExp(regexPattern);
116
+ // Check direct match
117
+ if (regex.test(relativePath)) {
118
+ return true;
119
+ }
120
+ // Check if any parent directory matches (for directory patterns)
121
+ const pathParts = relativePath.split(path.sep);
122
+ for (let i = 0; i < pathParts.length; i++) {
123
+ const partialPath = pathParts.slice(0, i + 1).join(path.sep);
124
+ if (regex.test(partialPath)) {
125
+ return true;
126
+ }
127
+ }
128
+ return false;
129
+ }
130
+ /**
131
+ * Convert a glob pattern to a regex pattern
132
+ */
133
+ patternToRegex(pattern) {
134
+ // Escape special regex characters except * and ?
135
+ let regex = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
136
+ // Handle ** globstar patterns first (before single *)
137
+ regex = regex.replace(/\*\*/g, '(?:.*[\\\\/])?.*'); // ** matches zero or more directories
138
+ // Convert remaining glob patterns to regex
139
+ regex = regex.replace(/\*/g, '[^\\\\/]*'); // * matches any characters except path separators
140
+ regex = regex.replace(/\?/g, '.'); // ? matches any single character
141
+ // Handle directory separators
142
+ regex = regex.replace(/\\\//g, '[\\\\/]'); // Handle both / and \ path separators
143
+ // For patterns ending with /, match anything under that directory
144
+ if (pattern.endsWith('/')) {
145
+ regex = regex.replace(/\$/, '(?:[\\\\/].*)?$');
146
+ }
147
+ // Anchor the pattern
148
+ if (!regex.startsWith('(?:.*[\\\\/])?.*') && !regex.startsWith('^')) {
149
+ regex = '^' + regex;
150
+ }
151
+ if (!regex.endsWith('.*') && !regex.endsWith('$')) {
152
+ regex = regex + '$';
153
+ }
154
+ return regex;
155
+ }
156
+ /**
157
+ * Get all loaded patterns (for debugging)
158
+ */
159
+ getPatterns() {
160
+ return [...this.patterns];
161
+ }
162
+ /**
163
+ * Filter a list of file paths, removing ignored ones
164
+ */
165
+ filterIgnored(filePaths, workingDirectory) {
166
+ return filePaths.filter(filePath => !this.shouldIgnore(filePath, workingDirectory));
167
+ }
168
+ async preFilterDirectory(workingDirectory) {
169
+ const result = {
170
+ totalFiles: 0,
171
+ filteredFiles: [],
172
+ excludedCount: 0,
173
+ trackableFiles: []
174
+ };
175
+ await this.scanDirectoryRecursive(workingDirectory, workingDirectory, result);
176
+ result.trackableFiles = result.filteredFiles.filter(filePath => this.isTrackableFile(filePath));
177
+ return result;
178
+ }
179
+ async scanDirectoryRecursive(dirPath, rootPath, result) {
180
+ try {
181
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
182
+ for (const entry of entries) {
183
+ const fullPath = path.join(dirPath, entry.name);
184
+ if (entry.isDirectory()) {
185
+ // Check if directory should be ignored before recursing
186
+ if (!this.shouldIgnore(fullPath, rootPath)) {
187
+ await this.scanDirectoryRecursive(fullPath, rootPath, result);
188
+ }
189
+ else {
190
+ try {
191
+ const excludedCount = await this.countFilesInDirectory(fullPath);
192
+ result.excludedCount += excludedCount;
193
+ }
194
+ catch (error) {
195
+ console.warn(`Failed to count files in excluded directory ${fullPath}:`, error);
196
+ }
197
+ }
198
+ }
199
+ else if (entry.isFile()) {
200
+ result.totalFiles++;
201
+ if (!this.shouldIgnore(fullPath, rootPath)) {
202
+ result.filteredFiles.push(fullPath);
203
+ }
204
+ else {
205
+ result.excludedCount++;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ catch (error) {
211
+ console.warn(`Failed to scan directory ${dirPath}:`, error);
212
+ }
213
+ }
214
+ async countFilesInDirectory(dirPath) {
215
+ try {
216
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
217
+ let count = 0;
218
+ for (const entry of entries) {
219
+ if (entry.isFile()) {
220
+ count++;
221
+ }
222
+ else if (entry.isDirectory()) {
223
+ count += await this.countFilesInDirectory(path.join(dirPath, entry.name));
224
+ }
225
+ }
226
+ return count;
227
+ }
228
+ catch (error) {
229
+ return 0;
230
+ }
231
+ }
232
+ isTrackableFile(filePath) {
233
+ const ext = path.extname(filePath).toLowerCase();
234
+ return this.trackableExtensions.includes(ext);
235
+ }
236
+ getPatternStats() {
237
+ return {
238
+ totalPatterns: this.patterns.length,
239
+ defaultPatterns: this.defaultPatterns.length,
240
+ customPatterns: Math.max(0, this.patterns.length - this.defaultPatterns.length)
241
+ };
242
+ }
243
+ /**
244
+ * Create a default .modignore file in the specified directory
245
+ */
246
+ static createDefaultIgnoreFile(workingDirectory) {
247
+ const ignoreFilePath = path.join(workingDirectory, '.modignore');
248
+ if (fs.existsSync(ignoreFilePath)) {
249
+ return; // Don't overwrite existing file
250
+ }
251
+ const defaultContent = `# Mod Ignore File
252
+ # Patterns to exclude from mod sync operations
253
+
254
+ # Dependencies
255
+ node_modules/
256
+ vendor/
257
+
258
+ # Build outputs
259
+ dist/
260
+ build/
261
+ out/
262
+ target/
263
+
264
+ # Environment files
265
+ .env
266
+ .env.local
267
+ .env.development.local
268
+ .env.test.local
269
+ .env.production.local
270
+
271
+ # Logs
272
+ *.log
273
+ logs/
274
+
275
+ # Runtime data
276
+ pids/
277
+ *.pid
278
+ *.seed
279
+ *.pid.lock
280
+
281
+ # Coverage directory used by tools like istanbul
282
+ coverage/
283
+ .nyc_output/
284
+
285
+ # Cache directories
286
+ .cache/
287
+ .npm/
288
+ .yarn/
289
+
290
+ # OS generated files
291
+ .DS_Store
292
+ .DS_Store?
293
+ ._*
294
+ .Spotlight-V100
295
+ .Trashes
296
+ ehthumbs.db
297
+ Thumbs.db
298
+
299
+ # Temporary files
300
+ *.tmp
301
+ *.temp
302
+ *.swp
303
+ *.swo
304
+ *~
305
+
306
+ # IDE files
307
+ .vscode/
308
+ .idea/
309
+ *.suo
310
+ *.ntvs*
311
+ *.njsproj
312
+ *.sln
313
+ *.sw?
314
+
315
+ # Automerge data
316
+ .automerge-data/
317
+ `;
318
+ try {
319
+ fs.writeFileSync(ignoreFilePath, defaultContent, 'utf8');
320
+ console.log('Created default .modignore file');
321
+ }
322
+ catch (err) {
323
+ console.warn('Warning: Could not create .modignore file:', err);
324
+ }
325
+ }
326
+ }
@@ -0,0 +1,244 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { fileURLToPath } from 'url';
5
+ import { readModConfig } from './mod-config.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ export class SyncDaemon {
9
+ constructor() {
10
+ this.syncQueue = [];
11
+ this.cacheDir = path.join(process.cwd(), '.mod', '.cache');
12
+ this.stateFilePath = path.join(this.cacheDir, 'sync-daemon.json');
13
+ this.pidFilePath = path.join(this.cacheDir, 'sync-daemon.pid');
14
+ }
15
+ async start(options = {}) {
16
+ try {
17
+ const currentState = await this.getState();
18
+ if (currentState && currentState.status === 'running' && !options.force) {
19
+ if (await this.isProcessRunning(currentState.pid)) {
20
+ throw new Error(`Daemon is already running (PID: ${currentState.pid})`);
21
+ }
22
+ }
23
+ await fs.mkdir(this.cacheDir, { recursive: true });
24
+ const cfg = readModConfig();
25
+ if (!cfg?.workspaceId) {
26
+ throw new Error('No active workspace found in .mod/config.json');
27
+ }
28
+ const daemonProcess = spawn(process.execPath, [
29
+ path.join(__dirname, '../daemon-worker.js'),
30
+ '--workspace-id', cfg.workspaceId,
31
+ '--active-branch', cfg.activeBranchId || 'main',
32
+ '--working-dir', process.cwd()
33
+ ], {
34
+ detached: true,
35
+ stdio: options.verbose ? 'inherit' : 'ignore',
36
+ cwd: process.cwd(),
37
+ env: { ...process.env, MOD_DAEMON_MODE: 'true' }
38
+ });
39
+ daemonProcess.unref();
40
+ const initialState = {
41
+ pid: daemonProcess.pid,
42
+ status: 'starting',
43
+ startedAt: new Date().toISOString(),
44
+ lastActivity: new Date().toISOString(),
45
+ workspaceId: cfg.workspaceId,
46
+ activeBranchId: cfg.activeBranchId,
47
+ watchedFiles: 0,
48
+ crashCount: 0,
49
+ uptime: 0
50
+ };
51
+ await this.saveState(initialState);
52
+ await fs.writeFile(this.pidFilePath, daemonProcess.pid.toString(), 'utf8');
53
+ console.log(`✅ Sync daemon started (PID: ${daemonProcess.pid})`);
54
+ console.log(`📁 Working directory: ${process.cwd()}`);
55
+ console.log(`🔗 Workspace: ${cfg.workspaceId}`);
56
+ await this.waitForDaemonReady(daemonProcess.pid, 5000);
57
+ }
58
+ catch (error) {
59
+ console.error('Failed to start sync daemon:', error);
60
+ throw error;
61
+ }
62
+ }
63
+ async stop(options = {}) {
64
+ try {
65
+ const currentState = await this.getState();
66
+ if (!currentState || currentState.status === 'stopped') {
67
+ console.log('Daemon is not running');
68
+ return;
69
+ }
70
+ if (!(await this.isProcessRunning(currentState.pid))) {
71
+ console.log('Daemon process not found (cleaning up stale state)');
72
+ await this.cleanup();
73
+ return;
74
+ }
75
+ console.log(`🛑 Stopping daemon (PID: ${currentState.pid})...`);
76
+ await this.updateState({ status: 'stopping' });
77
+ // Send SIGTERM for graceful shutdown
78
+ process.kill(currentState.pid, 'SIGTERM');
79
+ const shutdownTimeout = 10000; // 10 seconds
80
+ let gracefulShutdown = false;
81
+ for (let i = 0; i < shutdownTimeout / 100; i++) {
82
+ if (!(await this.isProcessRunning(currentState.pid))) {
83
+ gracefulShutdown = true;
84
+ break;
85
+ }
86
+ await new Promise(resolve => setTimeout(resolve, 100));
87
+ }
88
+ if (!gracefulShutdown) {
89
+ if (options.force) {
90
+ console.log('⚠️ Forcing daemon shutdown...');
91
+ process.kill(currentState.pid, 'SIGKILL');
92
+ }
93
+ else {
94
+ throw new Error('Daemon did not shut down gracefully. Use --force to kill the process.');
95
+ }
96
+ }
97
+ await this.cleanup();
98
+ console.log('✅ Sync daemon stopped');
99
+ }
100
+ catch (error) {
101
+ console.error('Failed to stop sync daemon:', error);
102
+ throw error;
103
+ }
104
+ }
105
+ async restart(options = {}) {
106
+ console.log('🔄 Restarting sync daemon...');
107
+ await this.stop(options);
108
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Brief pause
109
+ await this.start(options);
110
+ }
111
+ async status() {
112
+ try {
113
+ const state = await this.getState();
114
+ if (!state) {
115
+ return null;
116
+ }
117
+ const isRunning = await this.isProcessRunning(state.pid);
118
+ if (!isRunning && state.status !== 'stopped') {
119
+ // Process died unexpectedly
120
+ await this.updateState({
121
+ status: 'crashed',
122
+ lastError: 'Process terminated unexpectedly'
123
+ });
124
+ state.status = 'crashed';
125
+ }
126
+ // Calculate uptime if running
127
+ if (state.status === 'running') {
128
+ state.uptime = Date.now() - new Date(state.startedAt).getTime();
129
+ }
130
+ return state;
131
+ }
132
+ catch (error) {
133
+ console.error('Failed to get daemon status:', error);
134
+ return null;
135
+ }
136
+ }
137
+ async handleCrash(pid, error) {
138
+ const state = await this.getState();
139
+ if (!state)
140
+ return;
141
+ const crashCount = (state.crashCount || 0) + 1;
142
+ await this.updateState({
143
+ status: 'crashed',
144
+ crashCount,
145
+ lastError: error || 'Process crashed unexpectedly',
146
+ lastActivity: new Date().toISOString()
147
+ });
148
+ const backoffDelay = Math.min(1000 * Math.pow(2, crashCount - 1), 30000); // Max 30 seconds
149
+ console.error(`💥 Daemon crashed (attempt ${crashCount}). Restarting in ${backoffDelay}ms...`);
150
+ if (crashCount <= 5) { // Max 5 restart attempts
151
+ setTimeout(async () => {
152
+ try {
153
+ await this.start({ autoRestart: true });
154
+ }
155
+ catch (restartError) {
156
+ console.error('Failed to restart daemon after crash:', restartError);
157
+ }
158
+ }, backoffDelay);
159
+ }
160
+ else {
161
+ console.error('❌ Maximum crash restart attempts exceeded. Manual restart required.');
162
+ }
163
+ }
164
+ async addToSyncQueue(item) {
165
+ const queueItem = {
166
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
167
+ timestamp: new Date().toISOString(),
168
+ retryCount: 0,
169
+ ...item
170
+ };
171
+ if (item.priority === 'high') {
172
+ this.syncQueue.unshift(queueItem);
173
+ }
174
+ else {
175
+ this.syncQueue.push(queueItem);
176
+ }
177
+ await this.processSyncQueue();
178
+ }
179
+ async processSyncQueue() {
180
+ while (this.syncQueue.length > 0) {
181
+ const item = this.syncQueue.shift();
182
+ try {
183
+ // Process sync operation here
184
+ await this.updateState({ lastActivity: new Date().toISOString() });
185
+ }
186
+ catch (error) {
187
+ console.error(`Failed to process sync queue item ${item.id}:`, error);
188
+ if (item.retryCount < 3) {
189
+ item.retryCount++;
190
+ setTimeout(() => this.syncQueue.push(item), 1000 * item.retryCount);
191
+ }
192
+ }
193
+ }
194
+ }
195
+ async getState() {
196
+ try {
197
+ const data = await fs.readFile(this.stateFilePath, 'utf8');
198
+ return JSON.parse(data);
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ async saveState(state) {
205
+ await fs.writeFile(this.stateFilePath, JSON.stringify(state, null, 2), 'utf8');
206
+ }
207
+ async updateState(updates) {
208
+ const currentState = await this.getState();
209
+ if (currentState) {
210
+ const newState = { ...currentState, ...updates };
211
+ await this.saveState(newState);
212
+ }
213
+ }
214
+ async isProcessRunning(pid) {
215
+ try {
216
+ process.kill(pid, 0); // Signal 0 checks if process exists without killing
217
+ return true;
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ }
223
+ async waitForDaemonReady(pid, timeoutMs) {
224
+ const startTime = Date.now();
225
+ while (Date.now() - startTime < timeoutMs) {
226
+ const state = await this.getState();
227
+ if (state && state.pid === pid && state.status === 'running') {
228
+ return;
229
+ }
230
+ await new Promise(resolve => setTimeout(resolve, 100));
231
+ }
232
+ throw new Error('Daemon failed to start within timeout period');
233
+ }
234
+ async cleanup() {
235
+ try {
236
+ await fs.unlink(this.pidFilePath);
237
+ }
238
+ catch { /* ignore */ }
239
+ try {
240
+ await fs.unlink(this.stateFilePath);
241
+ }
242
+ catch { /* ignore */ }
243
+ }
244
+ }