@mod-computer/cli 0.2.4 → 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 (74) hide show
  1. package/package.json +3 -3
  2. package/dist/app.js +0 -227
  3. package/dist/cli.bundle.js.map +0 -7
  4. package/dist/cli.js +0 -132
  5. package/dist/commands/add.js +0 -245
  6. package/dist/commands/agents-run.js +0 -71
  7. package/dist/commands/auth.js +0 -259
  8. package/dist/commands/branch.js +0 -1411
  9. package/dist/commands/claude-sync.js +0 -772
  10. package/dist/commands/comment.js +0 -568
  11. package/dist/commands/diff.js +0 -182
  12. package/dist/commands/index.js +0 -73
  13. package/dist/commands/init.js +0 -597
  14. package/dist/commands/ls.js +0 -135
  15. package/dist/commands/members.js +0 -687
  16. package/dist/commands/mv.js +0 -282
  17. package/dist/commands/recover.js +0 -207
  18. package/dist/commands/rm.js +0 -257
  19. package/dist/commands/spec.js +0 -386
  20. package/dist/commands/status.js +0 -296
  21. package/dist/commands/sync.js +0 -119
  22. package/dist/commands/trace.js +0 -1752
  23. package/dist/commands/workspace.js +0 -447
  24. package/dist/components/conflict-resolution-ui.js +0 -120
  25. package/dist/components/messages.js +0 -5
  26. package/dist/components/thread.js +0 -8
  27. package/dist/config/features.js +0 -83
  28. package/dist/containers/branches-container.js +0 -140
  29. package/dist/containers/directory-container.js +0 -92
  30. package/dist/containers/thread-container.js +0 -214
  31. package/dist/containers/threads-container.js +0 -27
  32. package/dist/containers/workspaces-container.js +0 -27
  33. package/dist/daemon/conflict-resolution.js +0 -172
  34. package/dist/daemon/content-hash.js +0 -31
  35. package/dist/daemon/file-sync.js +0 -985
  36. package/dist/daemon/index.js +0 -203
  37. package/dist/daemon/mime-types.js +0 -166
  38. package/dist/daemon/offline-queue.js +0 -211
  39. package/dist/daemon/path-utils.js +0 -64
  40. package/dist/daemon/share-policy.js +0 -83
  41. package/dist/daemon/wasm-errors.js +0 -189
  42. package/dist/daemon/worker.js +0 -557
  43. package/dist/daemon-worker.js +0 -258
  44. package/dist/errors/workspace-errors.js +0 -48
  45. package/dist/lib/auth-server.js +0 -216
  46. package/dist/lib/browser.js +0 -35
  47. package/dist/lib/diff.js +0 -284
  48. package/dist/lib/formatters.js +0 -204
  49. package/dist/lib/git.js +0 -137
  50. package/dist/lib/local-fs.js +0 -201
  51. package/dist/lib/prompts.js +0 -56
  52. package/dist/lib/storage.js +0 -213
  53. package/dist/lib/trace-formatters.js +0 -314
  54. package/dist/services/add-service.js +0 -554
  55. package/dist/services/add-validation.js +0 -124
  56. package/dist/services/automatic-file-tracker.js +0 -303
  57. package/dist/services/cli-orchestrator.js +0 -227
  58. package/dist/services/feature-flags.js +0 -187
  59. package/dist/services/file-import-service.js +0 -283
  60. package/dist/services/file-transformation-service.js +0 -218
  61. package/dist/services/logger.js +0 -44
  62. package/dist/services/mod-config.js +0 -67
  63. package/dist/services/modignore-service.js +0 -328
  64. package/dist/services/sync-daemon.js +0 -244
  65. package/dist/services/thread-notification-service.js +0 -50
  66. package/dist/services/thread-service.js +0 -147
  67. package/dist/stores/use-directory-store.js +0 -96
  68. package/dist/stores/use-threads-store.js +0 -46
  69. package/dist/stores/use-workspaces-store.js +0 -54
  70. package/dist/types/add-types.js +0 -99
  71. package/dist/types/config.js +0 -16
  72. package/dist/types/index.js +0 -2
  73. package/dist/types/workspace-connection.js +0 -53
  74. 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
- }