@mod-computer/cli 0.2.3 → 0.2.5

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 (75) hide show
  1. package/dist/cli.bundle.js +216 -36371
  2. package/package.json +3 -3
  3. package/dist/app.js +0 -227
  4. package/dist/cli.bundle.js.map +0 -7
  5. package/dist/cli.js +0 -132
  6. package/dist/commands/add.js +0 -245
  7. package/dist/commands/agents-run.js +0 -71
  8. package/dist/commands/auth.js +0 -259
  9. package/dist/commands/branch.js +0 -1411
  10. package/dist/commands/claude-sync.js +0 -772
  11. package/dist/commands/comment.js +0 -568
  12. package/dist/commands/diff.js +0 -182
  13. package/dist/commands/index.js +0 -73
  14. package/dist/commands/init.js +0 -597
  15. package/dist/commands/ls.js +0 -135
  16. package/dist/commands/members.js +0 -687
  17. package/dist/commands/mv.js +0 -282
  18. package/dist/commands/recover.js +0 -207
  19. package/dist/commands/rm.js +0 -257
  20. package/dist/commands/spec.js +0 -386
  21. package/dist/commands/status.js +0 -296
  22. package/dist/commands/sync.js +0 -119
  23. package/dist/commands/trace.js +0 -1752
  24. package/dist/commands/workspace.js +0 -447
  25. package/dist/components/conflict-resolution-ui.js +0 -120
  26. package/dist/components/messages.js +0 -5
  27. package/dist/components/thread.js +0 -8
  28. package/dist/config/features.js +0 -83
  29. package/dist/containers/branches-container.js +0 -140
  30. package/dist/containers/directory-container.js +0 -92
  31. package/dist/containers/thread-container.js +0 -214
  32. package/dist/containers/threads-container.js +0 -27
  33. package/dist/containers/workspaces-container.js +0 -27
  34. package/dist/daemon/conflict-resolution.js +0 -172
  35. package/dist/daemon/content-hash.js +0 -31
  36. package/dist/daemon/file-sync.js +0 -985
  37. package/dist/daemon/index.js +0 -203
  38. package/dist/daemon/mime-types.js +0 -166
  39. package/dist/daemon/offline-queue.js +0 -211
  40. package/dist/daemon/path-utils.js +0 -64
  41. package/dist/daemon/share-policy.js +0 -83
  42. package/dist/daemon/wasm-errors.js +0 -189
  43. package/dist/daemon/worker.js +0 -557
  44. package/dist/daemon-worker.js +0 -258
  45. package/dist/errors/workspace-errors.js +0 -48
  46. package/dist/lib/auth-server.js +0 -216
  47. package/dist/lib/browser.js +0 -35
  48. package/dist/lib/diff.js +0 -284
  49. package/dist/lib/formatters.js +0 -204
  50. package/dist/lib/git.js +0 -137
  51. package/dist/lib/local-fs.js +0 -201
  52. package/dist/lib/prompts.js +0 -56
  53. package/dist/lib/storage.js +0 -213
  54. package/dist/lib/trace-formatters.js +0 -314
  55. package/dist/services/add-service.js +0 -554
  56. package/dist/services/add-validation.js +0 -124
  57. package/dist/services/automatic-file-tracker.js +0 -303
  58. package/dist/services/cli-orchestrator.js +0 -227
  59. package/dist/services/feature-flags.js +0 -187
  60. package/dist/services/file-import-service.js +0 -283
  61. package/dist/services/file-transformation-service.js +0 -218
  62. package/dist/services/logger.js +0 -44
  63. package/dist/services/mod-config.js +0 -67
  64. package/dist/services/modignore-service.js +0 -328
  65. package/dist/services/sync-daemon.js +0 -244
  66. package/dist/services/thread-notification-service.js +0 -50
  67. package/dist/services/thread-service.js +0 -147
  68. package/dist/stores/use-directory-store.js +0 -96
  69. package/dist/stores/use-threads-store.js +0 -46
  70. package/dist/stores/use-workspaces-store.js +0 -54
  71. package/dist/types/add-types.js +0 -99
  72. package/dist/types/config.js +0 -16
  73. package/dist/types/index.js +0 -2
  74. package/dist/types/workspace-connection.js +0 -53
  75. package/dist/types.js +0 -1
@@ -1,44 +0,0 @@
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
- }
@@ -1,67 +0,0 @@
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
- // NOTE: .mod directory creation disabled - config moved to ~/.mod/config
51
- // ensureModDir();
52
- const cfgPath = getConfigPath();
53
- // Only create directory if config path is not in cwd (e.g., using MOD_CONFIG_PATH override)
54
- const configDir = path.dirname(cfgPath);
55
- if (!configDir.includes(process.cwd()) && !fs.existsSync(configDir)) {
56
- fs.mkdirSync(configDir, { recursive: true });
57
- }
58
- const current = fs.existsSync(cfgPath)
59
- ? JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
60
- : {};
61
- const next = { ...current, ...update };
62
- fs.writeFileSync(cfgPath, JSON.stringify(next, null, 2), 'utf8');
63
- }
64
- catch (error) {
65
- console.error(`Failed to write mod config:`, error);
66
- }
67
- }
@@ -1,328 +0,0 @@
1
- // glassware[type="implementation", id="impl-cli-add-modignore-service--ae506406", requirements="requirement-cli-add-ignore-modignore--92ba9b60,requirement-cli-add-ignore-gitignore--054c4b53,requirement-cli-add-ignore-default--facbedfe,requirement-cli-add-ignore-negation--79b9bf48"]
2
- // spec: packages/mod-cli/specs/add.md
3
- import fs from 'fs';
4
- import path from 'path';
5
- export class ModIgnoreService {
6
- constructor(workingDirectory) {
7
- this.patterns = [];
8
- this.defaultPatterns = [
9
- 'node_modules/',
10
- '*/node_modules/*',
11
- '.git/',
12
- '.DS_Store',
13
- 'Thumbs.db',
14
- '*.log',
15
- 'npm-debug.log*',
16
- 'yarn-debug.log*',
17
- '.env',
18
- '.env.*',
19
- 'dist/',
20
- 'build/',
21
- '.next/',
22
- '.nuxt/',
23
- 'coverage/',
24
- '.nyc_output/',
25
- '*.tmp',
26
- '*.temp',
27
- '*.swp',
28
- '*.swo',
29
- '.cache/',
30
- '.vscode/',
31
- '.idea/',
32
- '.automerge-data/',
33
- 'package-lock.json',
34
- 'yarn.lock',
35
- 'pnpm-lock.yaml',
36
- '.mod/.cache',
37
- ];
38
- this.trackableExtensions = [
39
- '.md', '.txt', '.js', '.ts', '.jsx', '.tsx', '.json', '.yaml', '.yml',
40
- '.py', '.java', '.cpp', '.c', '.h', '.css', '.scss', '.html', '.xml',
41
- '.sql', '.go', '.rs', '.php', '.rb', '.swift', '.kt', '.scala'
42
- ];
43
- this.loadIgnoreFile(workingDirectory);
44
- }
45
- /**
46
- * Load .modignore file from the working directory
47
- */
48
- loadIgnoreFile(workingDirectory) {
49
- // Start with default patterns
50
- this.patterns = this.defaultPatterns.map(pattern => this.parsePattern(pattern));
51
- const ignoreFilePath = path.join(workingDirectory, '.modignore');
52
- try {
53
- if (fs.existsSync(ignoreFilePath)) {
54
- const content = fs.readFileSync(ignoreFilePath, 'utf8');
55
- const lines = content.split('\n')
56
- .map(line => line.trim())
57
- .filter(line => line && !line.startsWith('#')); // Remove comments and empty lines
58
- for (const line of lines) {
59
- this.patterns.push(this.parsePattern(line));
60
- }
61
- }
62
- }
63
- catch (err) {
64
- console.warn('Warning: Could not read .modignore file:', err);
65
- }
66
- }
67
- /**
68
- * Parse a single ignore pattern
69
- */
70
- parsePattern(pattern) {
71
- let cleanPattern = pattern.trim();
72
- const isNegation = cleanPattern.startsWith('!');
73
- if (isNegation) {
74
- cleanPattern = cleanPattern.slice(1);
75
- }
76
- const isDirectory = cleanPattern.endsWith('/');
77
- if (isDirectory) {
78
- cleanPattern = cleanPattern.slice(0, -1);
79
- }
80
- return {
81
- pattern: cleanPattern,
82
- isNegation,
83
- isDirectory,
84
- };
85
- }
86
- /**
87
- * Check if a file path should be ignored
88
- */
89
- shouldIgnore(filePath, workingDirectory) {
90
- // Always ignore .mod and .modignore files
91
- const relativePath = path.relative(workingDirectory, filePath);
92
- const fileName = path.basename(filePath);
93
- if (fileName === '.mod' || fileName === '.modignore') {
94
- return true;
95
- }
96
- // Check if it's a directory
97
- const isDirectory = fs.existsSync(filePath) && fs.statSync(filePath).isDirectory();
98
- let shouldIgnore = false;
99
- for (const { pattern, isNegation, isDirectory: patternIsDirectory } of this.patterns) {
100
- const matches = this.matchesPattern(relativePath, pattern, isDirectory, patternIsDirectory);
101
- if (matches) {
102
- shouldIgnore = !isNegation;
103
- }
104
- }
105
- return shouldIgnore;
106
- }
107
- /**
108
- * Check if a path matches a pattern
109
- */
110
- matchesPattern(relativePath, pattern, isDirectory, patternIsDirectory) {
111
- // If pattern is for directories only, skip non-directories
112
- if (patternIsDirectory && !isDirectory) {
113
- return false;
114
- }
115
- // Convert pattern to regex
116
- const regexPattern = this.patternToRegex(pattern);
117
- const regex = new RegExp(regexPattern);
118
- // Check direct match
119
- if (regex.test(relativePath)) {
120
- return true;
121
- }
122
- // Check if any parent directory matches (for directory patterns)
123
- const pathParts = relativePath.split(path.sep);
124
- for (let i = 0; i < pathParts.length; i++) {
125
- const partialPath = pathParts.slice(0, i + 1).join(path.sep);
126
- if (regex.test(partialPath)) {
127
- return true;
128
- }
129
- }
130
- return false;
131
- }
132
- /**
133
- * Convert a glob pattern to a regex pattern
134
- */
135
- patternToRegex(pattern) {
136
- // Escape special regex characters except * and ?
137
- let regex = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
138
- // Handle ** globstar patterns first (before single *)
139
- regex = regex.replace(/\*\*/g, '(?:.*[\\\\/])?.*'); // ** matches zero or more directories
140
- // Convert remaining glob patterns to regex
141
- regex = regex.replace(/\*/g, '[^\\\\/]*'); // * matches any characters except path separators
142
- regex = regex.replace(/\?/g, '.'); // ? matches any single character
143
- // Handle directory separators
144
- regex = regex.replace(/\\\//g, '[\\\\/]'); // Handle both / and \ path separators
145
- // For patterns ending with /, match anything under that directory
146
- if (pattern.endsWith('/')) {
147
- regex = regex.replace(/\$/, '(?:[\\\\/].*)?$');
148
- }
149
- // Anchor the pattern
150
- if (!regex.startsWith('(?:.*[\\\\/])?.*') && !regex.startsWith('^')) {
151
- regex = '^' + regex;
152
- }
153
- if (!regex.endsWith('.*') && !regex.endsWith('$')) {
154
- regex = regex + '$';
155
- }
156
- return regex;
157
- }
158
- /**
159
- * Get all loaded patterns (for debugging)
160
- */
161
- getPatterns() {
162
- return [...this.patterns];
163
- }
164
- /**
165
- * Filter a list of file paths, removing ignored ones
166
- */
167
- filterIgnored(filePaths, workingDirectory) {
168
- return filePaths.filter(filePath => !this.shouldIgnore(filePath, workingDirectory));
169
- }
170
- async preFilterDirectory(workingDirectory) {
171
- const result = {
172
- totalFiles: 0,
173
- filteredFiles: [],
174
- excludedCount: 0,
175
- trackableFiles: []
176
- };
177
- await this.scanDirectoryRecursive(workingDirectory, workingDirectory, result);
178
- result.trackableFiles = result.filteredFiles.filter(filePath => this.isTrackableFile(filePath));
179
- return result;
180
- }
181
- async scanDirectoryRecursive(dirPath, rootPath, result) {
182
- try {
183
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
184
- for (const entry of entries) {
185
- const fullPath = path.join(dirPath, entry.name);
186
- if (entry.isDirectory()) {
187
- // Check if directory should be ignored before recursing
188
- if (!this.shouldIgnore(fullPath, rootPath)) {
189
- await this.scanDirectoryRecursive(fullPath, rootPath, result);
190
- }
191
- else {
192
- try {
193
- const excludedCount = await this.countFilesInDirectory(fullPath);
194
- result.excludedCount += excludedCount;
195
- }
196
- catch (error) {
197
- console.warn(`Failed to count files in excluded directory ${fullPath}:`, error);
198
- }
199
- }
200
- }
201
- else if (entry.isFile()) {
202
- result.totalFiles++;
203
- if (!this.shouldIgnore(fullPath, rootPath)) {
204
- result.filteredFiles.push(fullPath);
205
- }
206
- else {
207
- result.excludedCount++;
208
- }
209
- }
210
- }
211
- }
212
- catch (error) {
213
- console.warn(`Failed to scan directory ${dirPath}:`, error);
214
- }
215
- }
216
- async countFilesInDirectory(dirPath) {
217
- try {
218
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
219
- let count = 0;
220
- for (const entry of entries) {
221
- if (entry.isFile()) {
222
- count++;
223
- }
224
- else if (entry.isDirectory()) {
225
- count += await this.countFilesInDirectory(path.join(dirPath, entry.name));
226
- }
227
- }
228
- return count;
229
- }
230
- catch (error) {
231
- return 0;
232
- }
233
- }
234
- isTrackableFile(filePath) {
235
- const ext = path.extname(filePath).toLowerCase();
236
- return this.trackableExtensions.includes(ext);
237
- }
238
- getPatternStats() {
239
- return {
240
- totalPatterns: this.patterns.length,
241
- defaultPatterns: this.defaultPatterns.length,
242
- customPatterns: Math.max(0, this.patterns.length - this.defaultPatterns.length)
243
- };
244
- }
245
- /**
246
- * Create a default .modignore file in the specified directory
247
- */
248
- static createDefaultIgnoreFile(workingDirectory) {
249
- const ignoreFilePath = path.join(workingDirectory, '.modignore');
250
- if (fs.existsSync(ignoreFilePath)) {
251
- return; // Don't overwrite existing file
252
- }
253
- const defaultContent = `# Mod Ignore File
254
- # Patterns to exclude from mod sync operations
255
-
256
- # Dependencies
257
- node_modules/
258
- vendor/
259
-
260
- # Build outputs
261
- dist/
262
- build/
263
- out/
264
- target/
265
-
266
- # Environment files
267
- .env
268
- .env.local
269
- .env.development.local
270
- .env.test.local
271
- .env.production.local
272
-
273
- # Logs
274
- *.log
275
- logs/
276
-
277
- # Runtime data
278
- pids/
279
- *.pid
280
- *.seed
281
- *.pid.lock
282
-
283
- # Coverage directory used by tools like istanbul
284
- coverage/
285
- .nyc_output/
286
-
287
- # Cache directories
288
- .cache/
289
- .npm/
290
- .yarn/
291
-
292
- # OS generated files
293
- .DS_Store
294
- .DS_Store?
295
- ._*
296
- .Spotlight-V100
297
- .Trashes
298
- ehthumbs.db
299
- Thumbs.db
300
-
301
- # Temporary files
302
- *.tmp
303
- *.temp
304
- *.swp
305
- *.swo
306
- *~
307
-
308
- # IDE files
309
- .vscode/
310
- .idea/
311
- *.suo
312
- *.ntvs*
313
- *.njsproj
314
- *.sln
315
- *.sw?
316
-
317
- # Automerge data
318
- .automerge-data/
319
- `;
320
- try {
321
- fs.writeFileSync(ignoreFilePath, defaultContent, 'utf8');
322
- console.log('Created default .modignore file');
323
- }
324
- catch (err) {
325
- console.warn('Warning: Could not create .modignore file:', err);
326
- }
327
- }
328
- }
@@ -1,244 +0,0 @@
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
- }