@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,557 +0,0 @@
1
- #!/usr/bin/env node
2
- // glassware[type="implementation", id="cli-daemon-worker--f5cd12a8", requirements="requirement-cli-sync-app-3--c2dca5d7,requirement-cli-sync-app-4--ed3c2529,requirement-cli-sync-app-5--a969b725,requirement-cli-sync-qual-3--99893467,requirement-cli-sync-qual-5--3d62024d,requirement-cli-sync-infra-1--4ec9e05c,requirement-cli-files-app-2--6765b2c1,requirement-cli-files-app-3--552aece6,requirement-cli-files-app-4--c5ef157e"]
3
- // glassware[type="implementation", id="impl-daemon-worker-distributed--2503a678", specifications="specification-spec-daemon-process--ed4facd5,specification-spec-daemon-websocket--708091f7,specification-spec-daemon-reconnect--1da6516e,specification-spec-daemon-backoff--0fa7d2ed,specification-spec-reconnect-logging--54499791,specification-spec-file-watch--ec0b56ec,specification-spec-offline-cli--c444a9a2,specification-spec-offline-persist--f4fc5073,specification-spec-sync-on-start--d1b0cc16"]
4
- // glassware[type="implementation", id="impl-daemon-offline-integration--4650abf8", requirements="requirement-cli-sync-offline-queue--08f04bd5,requirement-cli-sync-queue-persist--e35cadaa,requirement-cli-sync-queue-replay--a7954975"]
5
- // glassware[type="implementation", id="impl-daemon-wasm-integration--272a8dda", requirements="requirement-cli-sync-wasm-detect--5517bb61,requirement-cli-sync-wasm-recover--ca8c0cb6"]
6
- // glassware[type="implementation", id="impl-daemon-git-branch-scoping--7796ecba", requirements="requirement-cli-git-ux-2--e0fdbd0e,requirement-cli-git-app-4--ffac806a,requirement-cli-git-app-5--d0f0cbd5,requirement-cli-git-qual-3--5af20852"]
7
- /**
8
- * Daemon worker - runs as a background process watching directories.
9
- * Loads workspace connections from ~/.mod/workspaces/ and watches each.
10
- * Syncs file changes to Automerge documents in the workspace.
11
- *
12
- * Features:
13
- * - Offline queue: Changes are queued when offline and replayed on reconnect
14
- * - WASM error recovery: Automerge WASM errors are caught and retried
15
- * - Conflict resolution: Last-write-wins with conflict logging
16
- */
17
- import fs from 'fs';
18
- import path from 'path';
19
- import chokidar from 'chokidar';
20
- import { repo as getRepo } from '@mod/mod-core/repos/repo.node';
21
- import { cliSharePolicy } from './share-policy.js';
22
- import { listWorkspaceConnections, getSyncLogPath, getLogsDir, readConfig, } from '../lib/storage.js';
23
- import { detectGitRepo, readCurrentBranch, isGitOperationInProgress } from '../lib/git.js';
24
- import { createFileSyncContext, handleFileCreate, handleFileChange, handleFileDelete, performInitialSync, subscribeToRemoteChanges, cleanupRemoteSubscriptions, isPendingWrite, } from './file-sync.js';
25
- import { DEFAULT_IGNORE_PATTERNS, shouldIgnore } from '@mod/mod-core';
26
- import { OfflineQueueManager } from './offline-queue.js';
27
- import { WasmErrorRecovery } from './wasm-errors.js';
28
- import { normalizePath } from './path-utils.js';
29
- class DaemonWorker {
30
- constructor() {
31
- this.repo = null;
32
- this.watchers = new Map();
33
- this.headWatchers = new Map();
34
- this.fileSyncContexts = new Map();
35
- this.connections = [];
36
- this.debounceTimers = new Map();
37
- this.debounceMs = 100;
38
- this.isShuttingDown = false;
39
- this.gitBranches = new Map();
40
- // Run initial sync by default (can be disabled with MOD_INITIAL_SYNC=false)
41
- this.runInitialSync = process.env.MOD_INITIAL_SYNC !== 'false';
42
- // Track online/offline state
43
- this.isOnline = true;
44
- this.offlineQueue = new OfflineQueueManager((msg) => this.log(msg));
45
- this.wasmRecovery = new WasmErrorRecovery({
46
- maxRetries: 3,
47
- baseDelayMs: 100,
48
- logger: (msg) => this.log(msg),
49
- });
50
- }
51
- async start() {
52
- this.log('Daemon worker starting');
53
- // Initialize Automerge repo with CLI sharePolicy
54
- try {
55
- this.repo = await getRepo({ sharePolicy: cliSharePolicy });
56
- this.log('Automerge repo initialized with scoped sharePolicy');
57
- }
58
- catch (error) {
59
- this.log(`Failed to initialize Automerge repo: ${error?.message || error}`);
60
- process.exit(1);
61
- }
62
- // Load workspace connections
63
- this.connections = listWorkspaceConnections();
64
- if (this.connections.length === 0) {
65
- this.log('No workspace connections found, exiting');
66
- process.exit(0);
67
- }
68
- this.log(`Found ${this.connections.length} workspace connection(s)`);
69
- // Start watching each connected directory
70
- for (const connection of this.connections) {
71
- await this.watchDirectory(connection);
72
- }
73
- // Check auth state
74
- const config = readConfig();
75
- if (config.auth) {
76
- this.log(`Authenticated as ${config.auth.email}`);
77
- // Cloud sync is handled automatically by mod-core repo with WebSocket
78
- }
79
- else {
80
- this.log('Running in local-only mode (not authenticated)');
81
- }
82
- // Set up shutdown handlers
83
- process.on('SIGTERM', () => this.shutdown('SIGTERM'));
84
- process.on('SIGINT', () => this.shutdown('SIGINT'));
85
- // Periodically check for new workspace connections
86
- setInterval(() => this.checkForNewConnections(), 30000);
87
- // Start WASM error recovery periodic processing
88
- this.wasmRecovery.startPeriodicProcessing(1000);
89
- // Replay any queued offline changes
90
- if (this.offlineQueue.hasPending()) {
91
- this.log(`Found ${this.offlineQueue.getPendingCount()} pending offline changes, replaying...`);
92
- await this.replayOfflineQueue();
93
- }
94
- this.log('Daemon worker ready');
95
- }
96
- /**
97
- * Replay offline queue changes after reconnection.
98
- */
99
- async replayOfflineQueue() {
100
- const result = await this.offlineQueue.replay(async (change) => {
101
- const connection = this.connections.find(c => c.workspaceId === change.workspaceId);
102
- if (!connection) {
103
- this.log(`[offline-queue] No connection found for workspace ${change.workspaceId}`);
104
- return false;
105
- }
106
- const ctx = this.fileSyncContexts.get(connection.path);
107
- if (!ctx) {
108
- this.log(`[offline-queue] No sync context for ${connection.path}`);
109
- return false;
110
- }
111
- const absolutePath = path.join(connection.path, change.relativePath);
112
- try {
113
- switch (change.type) {
114
- case 'create':
115
- case 'update':
116
- if (fs.existsSync(absolutePath)) {
117
- await handleFileChange(ctx, absolutePath);
118
- }
119
- break;
120
- case 'delete':
121
- await handleFileDelete(ctx, absolutePath);
122
- break;
123
- }
124
- return true;
125
- }
126
- catch (error) {
127
- this.log(`[offline-queue] Error replaying ${change.relativePath}: ${error?.message || error}`);
128
- return false;
129
- }
130
- });
131
- this.log(`[offline-queue] Replay complete: ${result.synced} synced, ${result.failed} failed`);
132
- }
133
- async watchDirectory(connection) {
134
- if (!fs.existsSync(connection.path)) {
135
- this.log(`Directory not found: ${connection.path}, skipping`);
136
- return;
137
- }
138
- if (!this.repo) {
139
- this.log(`Repo not initialized, cannot watch ${connection.path}`);
140
- return;
141
- }
142
- // Detect git repo and branch
143
- const gitInfo = detectGitRepo(connection.path);
144
- const gitBranch = gitInfo.isGitRepo ? gitInfo.branch : null;
145
- this.log(`[git-detect] detectGitRepo(${connection.path}): isGitRepo=${gitInfo.isGitRepo}, branch=${gitInfo.branch}, headFile=${gitInfo.headFile}`);
146
- if (gitInfo.isGitRepo) {
147
- this.gitBranches.set(connection.path, gitBranch);
148
- this.log(`Watching: ${connection.path} → ${connection.workspaceName} (git branch: ${gitBranch || 'unknown'})`);
149
- // Watch .git/HEAD for branch changes
150
- if (gitInfo.headFile) {
151
- this.log(`[git-detect] Calling watchGitHead with headFile: ${gitInfo.headFile}`);
152
- this.watchGitHead(connection, gitInfo.headFile);
153
- }
154
- else {
155
- this.log(`[git-detect] No headFile found, skipping watchGitHead`);
156
- }
157
- }
158
- else {
159
- this.gitBranches.set(connection.path, null);
160
- this.log(`Watching: ${connection.path} → ${connection.workspaceName} (not a git repo)`);
161
- }
162
- // Create file sync context with logger
163
- try {
164
- const ctx = await createFileSyncContext(this.repo, connection, gitBranch, (msg) => this.log(msg));
165
- this.fileSyncContexts.set(connection.path, ctx);
166
- this.log(`File sync context created for ${connection.workspaceName}`);
167
- // Run initial sync if enabled
168
- if (this.runInitialSync) {
169
- const ignorePatterns = this.loadIgnorePatterns(connection.path);
170
- this.log(`Running initial sync for ${connection.workspaceName}...`);
171
- const stats = await performInitialSync(ctx, ignorePatterns);
172
- this.log(`Initial sync complete: ${stats.synced} files synced, ${stats.skipped} skipped`);
173
- }
174
- // Subscribe to remote changes for bidirectional sync
175
- await subscribeToRemoteChanges(ctx);
176
- }
177
- catch (error) {
178
- this.log(`Failed to create file sync context for ${connection.workspaceName}: ${error?.message || error}`);
179
- }
180
- // Load ignore patterns
181
- const ignorePatterns = this.loadIgnorePatterns(connection.path);
182
- const watcher = chokidar.watch(connection.path, {
183
- ignored: ignorePatterns,
184
- persistent: true,
185
- ignoreInitial: true,
186
- awaitWriteFinish: {
187
- stabilityThreshold: 100,
188
- pollInterval: 50,
189
- },
190
- });
191
- watcher.on('add', (filePath) => this.handleFileChange(connection, filePath, 'add'));
192
- watcher.on('change', (filePath) => this.handleFileChange(connection, filePath, 'change'));
193
- watcher.on('unlink', (filePath) => this.handleFileChange(connection, filePath, 'unlink'));
194
- watcher.on('error', (error) => {
195
- this.log(`Watcher error for ${connection.path}: ${error.message}`);
196
- });
197
- this.watchers.set(connection.path, watcher);
198
- }
199
- // glassware[type="implementation", id="impl-cli-watch-git-head--fde1da6d", specifications="specification-spec-watch-head--e3035ccf"]
200
- watchGitHead(connection, headFile) {
201
- // Watch the .git/HEAD file for branch changes
202
- this.log(`[git-head] Setting up HEAD watcher for ${connection.workspaceName}: ${headFile}`);
203
- const headWatcher = chokidar.watch(headFile, {
204
- persistent: true,
205
- ignoreInitial: true,
206
- });
207
- headWatcher.on('change', async () => {
208
- const newBranch = readCurrentBranch(headFile);
209
- const oldBranch = this.gitBranches.get(connection.path);
210
- this.log(`[git-head] HEAD file changed: ${headFile}, old=${oldBranch}, new=${newBranch}`);
211
- if (newBranch !== oldBranch) {
212
- await this.handleBranchSwitch(connection, oldBranch, newBranch);
213
- }
214
- });
215
- headWatcher.on('error', (error) => {
216
- this.log(`Git HEAD watcher error for ${connection.path}: ${error.message}`);
217
- });
218
- this.headWatchers.set(connection.path, headWatcher);
219
- }
220
- /**
221
- * Handle git branch switch.
222
- * Updates connection state, logs the change, and re-scopes sync.
223
- */
224
- // glassware[type="implementation", id="impl-cli-handle-branch-switch--87dd375f", specifications="specification-spec-branch-switch--243409fb"]
225
- async handleBranchSwitch(connection, oldBranch, newBranch) {
226
- // Update git branch tracking
227
- this.gitBranches.set(connection.path, newBranch);
228
- // Log branch change per spec requirement cli-git-ux-2
229
- this.log(`Git branch changed: ${oldBranch || 'unknown'} → ${newBranch || 'unknown'}`);
230
- this.log(`Re-scoping sync to branch: ${newBranch || 'default'}`);
231
- // Update file sync context with new branch
232
- const ctx = this.fileSyncContexts.get(connection.path);
233
- if (ctx) {
234
- // Update the context's git branch
235
- ctx.gitBranch = newBranch;
236
- // Record the branch detection so web app sees the change
237
- this.log(`[branch-switch] ctx.gitBranchService=${!!ctx.gitBranchService}, newBranch=${newBranch}`);
238
- if (ctx.gitBranchService && newBranch) {
239
- try {
240
- // Source ID must match pattern: cli:<alphanumeric-id>
241
- const clientId = connection.workspaceId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 20);
242
- const sourceId = `cli:${clientId}`;
243
- await ctx.gitBranchService.recordDetection(connection.workspaceId, sourceId, {
244
- branch: newBranch,
245
- revision: null,
246
- dirty: false,
247
- detached: false
248
- });
249
- this.log(`Recorded branch detection for: ${newBranch}`);
250
- }
251
- catch (error) {
252
- this.log(`Failed to record branch detection: ${error?.message || error}`);
253
- }
254
- }
255
- // Re-sync directory for new branch
256
- await this.resyncDirectory(connection, ctx);
257
- }
258
- }
259
- /**
260
- * Re-sync directory after branch switch.
261
- * Hydrates local state from workspace documents tagged with the current branch.
262
- *
263
- * spec-branch-switch--wip: Handles branch switching in CLI
264
- */
265
- // glassware[type="implementation", id="impl-daemon-resync-directory--43425a12", specifications="specification-spec-branch-switch--243409fb,specification-spec-git-operations--addf801f"]
266
- async resyncDirectory(connection, ctx) {
267
- const branch = ctx.gitBranch;
268
- const logPrefix = `[${connection.workspaceName}][${branch || 'default'}]`;
269
- this.log(`${logPrefix} Re-syncing directory for branch: ${branch || 'default'}`);
270
- // Check if a git operation is in progress - if so, wait briefly
271
- if (await isGitOperationInProgress(connection.path)) {
272
- this.log(`${logPrefix} Git operation in progress, waiting before resync...`);
273
- await new Promise(resolve => setTimeout(resolve, 1000));
274
- // Check again - if still in progress, skip this sync cycle
275
- if (await isGitOperationInProgress(connection.path)) {
276
- this.log(`${logPrefix} Git operation still in progress, skipping resync`);
277
- return;
278
- }
279
- }
280
- if (!branch) {
281
- this.log(`${logPrefix} No branch context, using default behavior`);
282
- return;
283
- }
284
- try {
285
- // Get the branch tree using listTree (works with the OLD format that applyOverride writes to)
286
- const treeResult = await ctx.workspace.gitBranch.listTree({ branch });
287
- const overrides = treeResult.overrides || {};
288
- const inherited = treeResult.inherited || {};
289
- this.log(`${logPrefix} Branch tree: ${Object.keys(overrides).length} overrides, ${Object.keys(inherited).length} inherited`);
290
- if (Object.keys(overrides).length > 0) {
291
- this.log(`${logPrefix} Override files: ${Object.keys(overrides).join(', ')}`);
292
- }
293
- // Combine overrides and inherited for full tree (path -> documentId)
294
- const allFiles = { ...inherited, ...overrides }; // overrides take precedence
295
- const inheritedPaths = new Set(Object.keys(inherited));
296
- // Clear existing caches to rebuild from branch state
297
- ctx.filePathCache.clear();
298
- ctx.docIdToPathCache.clear();
299
- // Populate caches from branch tree
300
- for (const [relativePath, docIdStr] of Object.entries(allFiles)) {
301
- const docId = docIdStr;
302
- ctx.filePathCache.set(relativePath, docId);
303
- ctx.docIdToPathCache.set(docId, relativePath);
304
- }
305
- // Materialize branch-specific files to local filesystem if they differ
306
- // Only materialize files that are NOT inherited (branch-specific changes)
307
- const branchSpecificPaths = Object.keys(overrides);
308
- this.log(`${logPrefix} ${branchSpecificPaths.length} branch-specific files to check`);
309
- for (const relativePath of branchSpecificPaths) {
310
- const docIdStr = overrides[relativePath];
311
- const absolutePath = path.join(connection.path, relativePath);
312
- try {
313
- // Get the document content
314
- const fileHandle = await ctx.workspace.file.get(docIdStr);
315
- if (!fileHandle) {
316
- this.log(`${logPrefix} Could not get file handle for: ${relativePath}`);
317
- continue;
318
- }
319
- const doc = fileHandle.doc();
320
- if (!doc)
321
- continue;
322
- // Check if local file exists and differs
323
- let shouldWrite = false;
324
- if (!fs.existsSync(absolutePath)) {
325
- shouldWrite = true;
326
- }
327
- else {
328
- // Compare content hash if available
329
- const localContent = fs.readFileSync(absolutePath);
330
- const localHash = (await import('./content-hash.js')).computeContentHash(localContent);
331
- const remoteHash = doc.metadata?.contentHash;
332
- if (remoteHash && localHash !== remoteHash) {
333
- shouldWrite = true;
334
- }
335
- }
336
- if (shouldWrite) {
337
- // Mark as pending write to avoid circular update
338
- ctx.pendingWrites.set(relativePath, Date.now() + 2000);
339
- // Ensure parent directory exists
340
- const parentDir = path.dirname(absolutePath);
341
- if (!fs.existsSync(parentDir)) {
342
- fs.mkdirSync(parentDir, { recursive: true });
343
- }
344
- // Write content to local filesystem
345
- const content = doc.text || doc.binary;
346
- if (doc.binary) {
347
- fs.writeFileSync(absolutePath, Buffer.from(doc.binary, 'base64'));
348
- }
349
- else if (content) {
350
- const { getTextContent } = await import('@mod/mod-core');
351
- const textContent = getTextContent(fileHandle, ['text']);
352
- fs.writeFileSync(absolutePath, textContent || '');
353
- }
354
- this.log(`${logPrefix} Materialized branch file: ${relativePath}`);
355
- }
356
- }
357
- catch (error) {
358
- this.log(`${logPrefix} Error materializing ${relativePath}: ${error?.message || error}`);
359
- }
360
- }
361
- this.log(`${logPrefix} Branch resync complete`);
362
- }
363
- catch (error) {
364
- this.log(`${logPrefix} Error during resync: ${error?.message || error}`);
365
- // Fall back to basic behavior
366
- this.log(`${logPrefix} Branch context updated, sync will use branch: ${branch || 'default'}`);
367
- }
368
- }
369
- loadIgnorePatterns(directoryPath) {
370
- const patterns = [...DEFAULT_IGNORE_PATTERNS];
371
- // Load .modignore if it exists
372
- const modignorePath = path.join(directoryPath, '.modignore');
373
- if (fs.existsSync(modignorePath)) {
374
- try {
375
- const content = fs.readFileSync(modignorePath, 'utf-8');
376
- const customPatterns = content
377
- .split('\n')
378
- .map((line) => line.trim())
379
- .filter((line) => line && !line.startsWith('#'));
380
- patterns.push(...customPatterns);
381
- }
382
- catch (error) {
383
- this.log(`Could not read .modignore: ${error?.message || error}`);
384
- }
385
- }
386
- return patterns;
387
- }
388
- handleFileChange(connection, filePath, event) {
389
- // Debounce rapid changes
390
- const key = filePath;
391
- const existingTimer = this.debounceTimers.get(key);
392
- if (existingTimer) {
393
- clearTimeout(existingTimer);
394
- }
395
- const timer = setTimeout(() => {
396
- this.debounceTimers.delete(key);
397
- this.processFileChange(connection, filePath, event);
398
- }, this.debounceMs);
399
- this.debounceTimers.set(key, timer);
400
- }
401
- async processFileChange(connection, filePath, event) {
402
- const relativePath = normalizePath(filePath, connection.path);
403
- const branch = this.gitBranches.get(connection.path);
404
- const branchPrefix = branch ? `[${branch}]` : '';
405
- const logPrefix = `[${connection.workspaceName}]${branchPrefix}`;
406
- // Defensive check: skip ignored files (chokidar should filter, but double-check)
407
- if (shouldIgnore(relativePath)) {
408
- // Don't log to avoid noise - this is just a safety net
409
- return;
410
- }
411
- // Get file sync context
412
- const ctx = this.fileSyncContexts.get(connection.path);
413
- if (!ctx) {
414
- this.log(`${logPrefix} No sync context, skipping: ${relativePath}`);
415
- return;
416
- }
417
- // Skip if this is a pending write from remote sync (avoid circular updates)
418
- if (isPendingWrite(ctx, relativePath)) {
419
- this.log(`${logPrefix} Skipping pending write: ${relativePath}`);
420
- return;
421
- }
422
- // Don't sync during active git operations (rebase, merge, etc.)
423
- if (isGitOperationInProgress(connection.path)) {
424
- this.log(`${logPrefix} Git operation in progress, skipping: ${relativePath}`);
425
- return;
426
- }
427
- // If offline, queue the change for later
428
- if (!this.isOnline) {
429
- this.log(`${logPrefix} Offline, queuing: ${relativePath}`);
430
- this.offlineQueue.add({
431
- type: event === 'add' ? 'create' : event === 'unlink' ? 'delete' : 'update',
432
- relativePath,
433
- workspaceId: connection.workspaceId,
434
- });
435
- return;
436
- }
437
- // Execute with WASM error recovery
438
- const result = await this.wasmRecovery.executeWithRecovery(`${connection.workspaceId}:${relativePath}`, async () => {
439
- switch (event) {
440
- case 'add':
441
- this.log(`${logPrefix} File added: ${relativePath}`);
442
- await handleFileCreate(ctx, filePath);
443
- break;
444
- case 'change':
445
- this.log(`${logPrefix} File changed: ${relativePath}`);
446
- await handleFileChange(ctx, filePath);
447
- break;
448
- case 'unlink':
449
- this.log(`${logPrefix} File deleted: ${relativePath}`);
450
- await handleFileDelete(ctx, filePath);
451
- break;
452
- }
453
- return true;
454
- });
455
- if (result === null) {
456
- // WASM error occurred, already queued for retry
457
- this.log(`${logPrefix} WASM error for ${relativePath}, queued for retry`);
458
- }
459
- }
460
- /**
461
- * Set online/offline state. When going online, replay queued changes.
462
- */
463
- async setOnlineState(online) {
464
- if (this.isOnline === online)
465
- return;
466
- this.isOnline = online;
467
- this.log(`Connection state changed: ${online ? 'online' : 'offline'}`);
468
- if (online && this.offlineQueue.hasPending()) {
469
- this.log('Back online, replaying offline queue...');
470
- await this.replayOfflineQueue();
471
- }
472
- }
473
- async checkForNewConnections() {
474
- const currentConnections = listWorkspaceConnections();
475
- // Check for new connections
476
- for (const connection of currentConnections) {
477
- if (!this.watchers.has(connection.path)) {
478
- this.log(`New workspace connection detected: ${connection.path}`);
479
- await this.watchDirectory(connection);
480
- }
481
- }
482
- // Check for removed connections
483
- for (const watchedPath of this.watchers.keys()) {
484
- if (!currentConnections.some((c) => c.path === watchedPath)) {
485
- this.log(`Workspace connection removed: ${watchedPath}`);
486
- const watcher = this.watchers.get(watchedPath);
487
- if (watcher) {
488
- await watcher.close();
489
- this.watchers.delete(watchedPath);
490
- }
491
- // Also cleanup git HEAD watcher
492
- const headWatcher = this.headWatchers.get(watchedPath);
493
- if (headWatcher) {
494
- await headWatcher.close();
495
- this.headWatchers.delete(watchedPath);
496
- }
497
- // Cleanup file sync context
498
- this.fileSyncContexts.delete(watchedPath);
499
- this.gitBranches.delete(watchedPath);
500
- }
501
- }
502
- this.connections = currentConnections;
503
- }
504
- async shutdown(signal) {
505
- if (this.isShuttingDown)
506
- return;
507
- this.isShuttingDown = true;
508
- this.log(`Received ${signal}, shutting down...`);
509
- // Stop WASM recovery processing
510
- this.wasmRecovery.stopPeriodicProcessing();
511
- this.log('Stopped WASM recovery processing');
512
- // Cleanup remote change subscriptions
513
- for (const [watchPath, ctx] of this.fileSyncContexts) {
514
- cleanupRemoteSubscriptions(ctx);
515
- this.log(`Cleaned up subscriptions for ${watchPath}`);
516
- }
517
- // Close all file watchers
518
- for (const [watchPath, watcher] of this.watchers) {
519
- await watcher.close();
520
- this.log(`Closed watcher for ${watchPath}`);
521
- }
522
- // Close all git HEAD watchers
523
- for (const [watchPath, watcher] of this.headWatchers) {
524
- await watcher.close();
525
- this.log(`Closed git HEAD watcher for ${watchPath}`);
526
- }
527
- // Clear debounce timers
528
- for (const timer of this.debounceTimers.values()) {
529
- clearTimeout(timer);
530
- }
531
- this.log('Daemon worker stopped');
532
- process.exit(0);
533
- }
534
- log(message) {
535
- const timestamp = new Date().toISOString();
536
- const line = `${timestamp} [INFO] ${message}\n`;
537
- // Write to log file
538
- try {
539
- const logPath = getSyncLogPath();
540
- const logDir = getLogsDir();
541
- if (!fs.existsSync(logDir)) {
542
- fs.mkdirSync(logDir, { recursive: true });
543
- }
544
- fs.appendFileSync(logPath, line);
545
- }
546
- catch (error) {
547
- // Fall back to console if logging fails
548
- console.log(line);
549
- }
550
- }
551
- }
552
- // Start the daemon worker
553
- const worker = new DaemonWorker();
554
- worker.start().catch((error) => {
555
- console.error('Daemon worker failed to start:', error);
556
- process.exit(1);
557
- });