@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,257 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import chokidar from 'chokidar';
6
+ import { AutomaticFileTracker } from './services/automatic-file-tracker.js';
7
+ import { createModWorkspace } from '@mod/mod-core';
8
+ import { repo as getRepo } from '@mod/mod-core/repos/repo.node';
9
+ import { readModConfig } from './services/mod-config.js';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ class DaemonWorker {
13
+ constructor(options) {
14
+ this.isShuttingDown = false;
15
+ this.options = options;
16
+ this.stateFilePath = path.join(options.workingDir, '.mod', '.cache', 'sync-daemon.json');
17
+ }
18
+ async start() {
19
+ try {
20
+ process.on('SIGTERM', () => this.gracefulShutdown());
21
+ process.on('SIGINT', () => this.gracefulShutdown());
22
+ process.on('uncaughtException', (error) => this.handleError(error));
23
+ process.on('unhandledRejection', (error) => this.handleError(error));
24
+ await this.updateState({
25
+ status: 'running',
26
+ lastActivity: new Date().toISOString(),
27
+ watchedFiles: 0
28
+ });
29
+ if (this.options.verbose) {
30
+ console.log(`🚀 Daemon worker starting...`);
31
+ console.log(`📁 Working directory: ${this.options.workingDir}`);
32
+ console.log(`🔗 Workspace ID: ${this.options.workspaceId}`);
33
+ console.log(`🌿 Active branch: ${this.options.activeBranchId}`);
34
+ }
35
+ // Initialize repo and workspace (this would need proper repo setup)
36
+ // For now, using placeholder implementation
37
+ const repo = await this.initializeRepo();
38
+ const modWorkspace = createModWorkspace(repo);
39
+ const workspaceHandles = await modWorkspace.listWorkspaces();
40
+ this.workspaceHandle = workspaceHandles.find(wh => wh.id === this.options.workspaceId);
41
+ if (!this.workspaceHandle) {
42
+ throw new Error(`Workspace ${this.options.workspaceId} not found`);
43
+ }
44
+ // Set active branch
45
+ if (this.options.activeBranchId) {
46
+ try {
47
+ await this.workspaceHandle.branch.switchActive(this.options.activeBranchId);
48
+ }
49
+ catch (error) {
50
+ console.warn(`⚠️ Failed to set active branch: ${error}`);
51
+ }
52
+ }
53
+ await this.setupConfigWatcher();
54
+ this.tracker = new AutomaticFileTracker(repo);
55
+ await this.tracker.enableAutoTracking({
56
+ workspaceId: this.options.workspaceId,
57
+ workspaceHandle: this.workspaceHandle,
58
+ watchDirectory: this.options.workingDir,
59
+ debounceMs: 500,
60
+ verbose: this.options.verbose || false,
61
+ maxWatchedFiles: 2000,
62
+ preFilterEnabled: true,
63
+ comprehensiveMode: true,
64
+ resourceMonitoring: true
65
+ });
66
+ const status = this.tracker.getTrackingStatus();
67
+ await this.updateState({
68
+ watchedFiles: status.watchedFiles,
69
+ lastActivity: new Date().toISOString()
70
+ });
71
+ if (this.options.verbose) {
72
+ console.log(`✅ Daemon worker running (PID: ${process.pid})`);
73
+ console.log(`📊 Watching ${status.watchedFiles} files`);
74
+ }
75
+ await this.keepAlive();
76
+ }
77
+ catch (error) {
78
+ await this.handleError(error);
79
+ process.exit(1);
80
+ }
81
+ }
82
+ async gracefulShutdown() {
83
+ if (this.isShuttingDown)
84
+ return;
85
+ this.isShuttingDown = true;
86
+ console.log('\n🛑 Daemon worker shutting down...');
87
+ try {
88
+ await this.updateState({
89
+ status: 'stopping',
90
+ lastActivity: new Date().toISOString()
91
+ });
92
+ if (this.tracker) {
93
+ await this.tracker.disableAutoTracking();
94
+ }
95
+ if (this.configWatcher) {
96
+ await this.configWatcher.close();
97
+ }
98
+ await this.updateState({
99
+ status: 'stopped',
100
+ lastActivity: new Date().toISOString()
101
+ });
102
+ console.log('✅ Daemon worker stopped gracefully');
103
+ process.exit(0);
104
+ }
105
+ catch (error) {
106
+ console.error('Error during graceful shutdown:', error);
107
+ process.exit(1);
108
+ }
109
+ }
110
+ async handleError(error) {
111
+ console.error('💥 Daemon worker error:', error);
112
+ try {
113
+ await this.updateState({
114
+ status: 'crashed',
115
+ lastError: error.message || 'Unknown error',
116
+ lastActivity: new Date().toISOString()
117
+ });
118
+ }
119
+ catch (stateError) {
120
+ console.error('Failed to update error state:', stateError);
121
+ }
122
+ // Let the main daemon handle restart logic
123
+ process.exit(1);
124
+ }
125
+ async keepAlive() {
126
+ while (!this.isShuttingDown) {
127
+ try {
128
+ await this.updateState({
129
+ lastActivity: new Date().toISOString()
130
+ });
131
+ // Check if tracking is still active
132
+ if (this.tracker) {
133
+ const status = this.tracker.getTrackingStatus();
134
+ if (status.watchedFiles !== undefined) {
135
+ await this.updateState({ watchedFiles: status.watchedFiles });
136
+ }
137
+ }
138
+ await new Promise(resolve => setTimeout(resolve, 10000)); // 10 second intervals
139
+ }
140
+ catch (error) {
141
+ await this.handleError(error);
142
+ break;
143
+ }
144
+ }
145
+ }
146
+ async updateState(updates) {
147
+ try {
148
+ let currentState = {};
149
+ try {
150
+ const data = await fs.readFile(this.stateFilePath, 'utf8');
151
+ currentState = JSON.parse(data);
152
+ }
153
+ catch { /* State file doesn't exist yet */ }
154
+ const newState = { ...currentState, ...updates, pid: process.pid };
155
+ await fs.writeFile(this.stateFilePath, JSON.stringify(newState, null, 2), 'utf8');
156
+ }
157
+ catch (error) {
158
+ console.error('Failed to update daemon state:', error);
159
+ }
160
+ }
161
+ async setupConfigWatcher() {
162
+ const configPath = path.join(this.options.workingDir, '.mod', 'config.json');
163
+ this.configWatcher = chokidar.watch(configPath, {
164
+ persistent: true,
165
+ ignoreInitial: true
166
+ });
167
+ this.configWatcher.on('change', async () => {
168
+ try {
169
+ await this.handleConfigChange();
170
+ }
171
+ catch (error) {
172
+ console.error('Error handling config change:', error);
173
+ }
174
+ });
175
+ if (this.options.verbose) {
176
+ console.log(`🔍 Watching config file: ${configPath}`);
177
+ }
178
+ }
179
+ async handleConfigChange() {
180
+ try {
181
+ // Read the updated config
182
+ const config = readModConfig();
183
+ if (!config?.activeBranchId) {
184
+ return;
185
+ }
186
+ // Check if the branch has actually changed
187
+ if (config.activeBranchId === this.options.activeBranchId) {
188
+ return;
189
+ }
190
+ if (this.options.verbose) {
191
+ console.log(`🔄 Branch switch detected: ${this.options.activeBranchId} → ${config.activeBranchId}`);
192
+ }
193
+ if (this.workspaceHandle) {
194
+ try {
195
+ await this.workspaceHandle.branch.switchActive(config.activeBranchId);
196
+ this.options.activeBranchId = config.activeBranchId;
197
+ await this.updateState({
198
+ activeBranchId: config.activeBranchId,
199
+ lastActivity: new Date().toISOString()
200
+ });
201
+ if (this.options.verbose) {
202
+ console.log(`✅ Switched to branch: ${config.activeBranchId}`);
203
+ }
204
+ }
205
+ catch (error) {
206
+ console.error(`❌ Failed to switch to branch ${config.activeBranchId}:`, error);
207
+ }
208
+ }
209
+ }
210
+ catch (error) {
211
+ console.error('Error reading config during branch switch:', error);
212
+ }
213
+ }
214
+ async initializeRepo() {
215
+ // Use the same repo initialization as the main CLI
216
+ return await getRepo();
217
+ }
218
+ }
219
+ if (import.meta.url === `file://${process.argv[1]}`) {
220
+ const args = process.argv.slice(2);
221
+ const options = {
222
+ workspaceId: '',
223
+ activeBranchId: 'main',
224
+ workingDir: process.cwd(),
225
+ verbose: false
226
+ };
227
+ // Parse command line arguments
228
+ for (let i = 0; i < args.length; i += 2) {
229
+ const key = args[i];
230
+ const value = args[i + 1];
231
+ switch (key) {
232
+ case '--workspace-id':
233
+ options.workspaceId = value;
234
+ break;
235
+ case '--active-branch':
236
+ options.activeBranchId = value;
237
+ break;
238
+ case '--working-dir':
239
+ options.workingDir = value;
240
+ break;
241
+ case '--verbose':
242
+ options.verbose = true;
243
+ i--; // No value for this flag
244
+ break;
245
+ }
246
+ }
247
+ if (!options.workspaceId) {
248
+ console.error('❌ Workspace ID is required');
249
+ process.exit(1);
250
+ }
251
+ const worker = new DaemonWorker(options);
252
+ worker.start().catch((error) => {
253
+ console.error('❌ Failed to start daemon worker:', error);
254
+ process.exit(1);
255
+ });
256
+ }
257
+ export { DaemonWorker };
@@ -0,0 +1,153 @@
1
+ // glassware[type=implementation, id=cli-auth-server, requirements=req-cli-auth-app-2,req-cli-auth-app-4,req-cli-auth-qual-1]
2
+ import http from 'http';
3
+ import { URL } from 'url';
4
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
5
+ /**
6
+ * Start a localhost HTTP server to receive OAuth callback.
7
+ * Returns the port number and a promise that resolves with auth result.
8
+ */
9
+ export async function startAuthServer(options = {}) {
10
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
11
+ let resolveAuth;
12
+ let rejectAuth;
13
+ const resultPromise = new Promise((resolve, reject) => {
14
+ resolveAuth = resolve;
15
+ rejectAuth = reject;
16
+ });
17
+ const server = http.createServer((req, res) => {
18
+ const url = new URL(req.url || '/', `http://localhost`);
19
+ if (url.pathname === '/callback') {
20
+ const token = url.searchParams.get('token');
21
+ const email = url.searchParams.get('email');
22
+ const name = url.searchParams.get('name');
23
+ const googleId = url.searchParams.get('googleId');
24
+ if (token && email && googleId) {
25
+ // Send success page to browser
26
+ res.writeHead(200, { 'Content-Type': 'text/html' });
27
+ res.end(getSuccessPage(name || email));
28
+ // Resolve with auth result
29
+ resolveAuth({
30
+ googleIdToken: token,
31
+ googleId,
32
+ email,
33
+ name: name || email.split('@')[0],
34
+ });
35
+ }
36
+ else {
37
+ // Missing parameters
38
+ res.writeHead(400, { 'Content-Type': 'text/html' });
39
+ res.end(getErrorPage('Missing authentication parameters'));
40
+ rejectAuth(new Error('Missing authentication parameters in callback'));
41
+ }
42
+ }
43
+ else {
44
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
45
+ res.end('Not found');
46
+ }
47
+ });
48
+ // Wait for server to start listening
49
+ const port = await new Promise((resolve, reject) => {
50
+ server.on('error', reject);
51
+ server.listen(0, '127.0.0.1', () => {
52
+ const address = server.address();
53
+ const port = typeof address === 'object' && address ? address.port : 0;
54
+ resolve(port);
55
+ });
56
+ });
57
+ // Set timeout
58
+ const timeoutId = setTimeout(() => {
59
+ server.close();
60
+ rejectAuth(new Error('Authentication timed out after 5 minutes'));
61
+ }, timeoutMs);
62
+ // Close server when auth completes
63
+ resultPromise.finally(() => {
64
+ clearTimeout(timeoutId);
65
+ server.close();
66
+ });
67
+ return {
68
+ port,
69
+ result: resultPromise,
70
+ close: () => {
71
+ clearTimeout(timeoutId);
72
+ server.close();
73
+ rejectAuth(new Error('Authentication cancelled'));
74
+ },
75
+ };
76
+ }
77
+ function getSuccessPage(name) {
78
+ return `<!DOCTYPE html>
79
+ <html>
80
+ <head>
81
+ <title>Signed in to Mod</title>
82
+ <style>
83
+ body {
84
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
85
+ display: flex;
86
+ justify-content: center;
87
+ align-items: center;
88
+ height: 100vh;
89
+ margin: 0;
90
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
91
+ color: white;
92
+ }
93
+ .container {
94
+ text-align: center;
95
+ padding: 2rem;
96
+ background: rgba(255,255,255,0.1);
97
+ border-radius: 12px;
98
+ backdrop-filter: blur(10px);
99
+ }
100
+ h1 { margin-bottom: 0.5rem; }
101
+ p { opacity: 0.9; }
102
+ .check { font-size: 3rem; margin-bottom: 1rem; }
103
+ </style>
104
+ </head>
105
+ <body>
106
+ <div class="container">
107
+ <div class="check">✓</div>
108
+ <h1>Welcome, ${escapeHtml(name)}!</h1>
109
+ <p>You can close this window and return to your terminal.</p>
110
+ </div>
111
+ </body>
112
+ </html>`;
113
+ }
114
+ function getErrorPage(message) {
115
+ return `<!DOCTYPE html>
116
+ <html>
117
+ <head>
118
+ <title>Authentication Error</title>
119
+ <style>
120
+ body {
121
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
122
+ display: flex;
123
+ justify-content: center;
124
+ align-items: center;
125
+ height: 100vh;
126
+ margin: 0;
127
+ background: #1a1a2e;
128
+ color: white;
129
+ }
130
+ .container {
131
+ text-align: center;
132
+ padding: 2rem;
133
+ }
134
+ h1 { color: #ff6b6b; }
135
+ </style>
136
+ </head>
137
+ <body>
138
+ <div class="container">
139
+ <h1>Authentication Error</h1>
140
+ <p>${escapeHtml(message)}</p>
141
+ <p>Please try again from your terminal.</p>
142
+ </div>
143
+ </body>
144
+ </html>`;
145
+ }
146
+ function escapeHtml(str) {
147
+ return str
148
+ .replace(/&/g, '&amp;')
149
+ .replace(/</g, '&lt;')
150
+ .replace(/>/g, '&gt;')
151
+ .replace(/"/g, '&quot;')
152
+ .replace(/'/g, '&#039;');
153
+ }
@@ -0,0 +1,35 @@
1
+ // glassware[type=implementation, id=cli-browser-opener, requirements=req-cli-auth-ux-2,req-cli-auth-ux-4]
2
+ import { exec } from 'child_process';
3
+ import { platform } from 'os';
4
+ /**
5
+ * Open a URL in the default browser.
6
+ * Returns true if successful, false if browser couldn't be opened.
7
+ */
8
+ export async function openBrowser(url) {
9
+ const command = getBrowserCommand(url);
10
+ return new Promise((resolve) => {
11
+ exec(command, (error) => {
12
+ if (error) {
13
+ resolve(false);
14
+ }
15
+ else {
16
+ resolve(true);
17
+ }
18
+ });
19
+ });
20
+ }
21
+ /**
22
+ * Get the platform-specific command to open a URL.
23
+ */
24
+ function getBrowserCommand(url) {
25
+ const escapedUrl = url.replace(/"/g, '\\"');
26
+ switch (platform()) {
27
+ case 'darwin':
28
+ return `open "${escapedUrl}"`;
29
+ case 'win32':
30
+ return `start "" "${escapedUrl}"`;
31
+ default:
32
+ // Linux and others - try xdg-open
33
+ return `xdg-open "${escapedUrl}"`;
34
+ }
35
+ }
@@ -0,0 +1,203 @@
1
+ // glassware[type=implementation, id=cli-storage-utils, requirements=req-cli-storage-struct-1,req-cli-storage-app-1,req-cli-storage-app-2,req-cli-storage-app-3,req-cli-storage-conn-2,req-cli-storage-qual-1,req-cli-storage-qual-5]
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import os from 'os';
6
+ import { DEFAULT_CONFIG, DEFAULT_SETTINGS } from '../types/config.js';
7
+ /**
8
+ * Get the path to the ~/.mod/ directory.
9
+ */
10
+ export function getModDir() {
11
+ return path.join(os.homedir(), '.mod');
12
+ }
13
+ /**
14
+ * Ensure the ~/.mod/ directory structure exists.
15
+ * Creates: ~/.mod/, ~/.mod/workspaces/, ~/.mod/automerge/, ~/.mod/logs/
16
+ */
17
+ export function ensureModDir() {
18
+ const modDir = getModDir();
19
+ const dirs = [
20
+ modDir,
21
+ path.join(modDir, 'workspaces'),
22
+ path.join(modDir, 'automerge'),
23
+ path.join(modDir, 'logs'),
24
+ ];
25
+ for (const dir of dirs) {
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
28
+ }
29
+ }
30
+ }
31
+ /**
32
+ * Get the path to the config file.
33
+ */
34
+ export function getConfigPath() {
35
+ return path.join(getModDir(), 'config');
36
+ }
37
+ /**
38
+ * Read the global config from ~/.mod/config.
39
+ * Returns default config if file doesn't exist or is invalid.
40
+ */
41
+ export function readConfig() {
42
+ const configPath = getConfigPath();
43
+ if (!fs.existsSync(configPath)) {
44
+ return { ...DEFAULT_CONFIG };
45
+ }
46
+ try {
47
+ const content = fs.readFileSync(configPath, 'utf-8');
48
+ const stored = JSON.parse(content);
49
+ // Merge with defaults for missing fields
50
+ return {
51
+ version: stored.version ?? DEFAULT_CONFIG.version,
52
+ auth: stored.auth ?? null,
53
+ settings: {
54
+ ...DEFAULT_SETTINGS,
55
+ ...stored.settings,
56
+ },
57
+ };
58
+ }
59
+ catch (error) {
60
+ // Return defaults if file is corrupted
61
+ console.warn('Warning: Could not read ~/.mod/config, using defaults');
62
+ return { ...DEFAULT_CONFIG };
63
+ }
64
+ }
65
+ /**
66
+ * Write config to ~/.mod/config atomically.
67
+ * Creates the file with 0600 permissions (user read/write only).
68
+ */
69
+ export function writeConfig(config) {
70
+ ensureModDir();
71
+ const configPath = getConfigPath();
72
+ atomicWrite(configPath, JSON.stringify(config, null, 2), 0o600);
73
+ }
74
+ /**
75
+ * Get the path to a workspace connection file for a directory.
76
+ * Uses SHA-256 hash of the absolute path (first 16 chars).
77
+ */
78
+ export function getWorkspaceConnectionPath(directoryPath) {
79
+ const absolutePath = path.resolve(directoryPath);
80
+ const hash = crypto
81
+ .createHash('sha256')
82
+ .update(absolutePath)
83
+ .digest('hex')
84
+ .slice(0, 16);
85
+ return path.join(getModDir(), 'workspaces', hash);
86
+ }
87
+ /**
88
+ * Read the workspace connection for a directory.
89
+ * Returns null if no connection exists.
90
+ */
91
+ export function readWorkspaceConnection(directoryPath) {
92
+ const connectionPath = getWorkspaceConnectionPath(directoryPath);
93
+ if (!fs.existsSync(connectionPath)) {
94
+ return null;
95
+ }
96
+ try {
97
+ const content = fs.readFileSync(connectionPath, 'utf-8');
98
+ return JSON.parse(content);
99
+ }
100
+ catch (error) {
101
+ console.warn(`Warning: Could not read workspace connection for ${directoryPath}`);
102
+ return null;
103
+ }
104
+ }
105
+ /**
106
+ * Write a workspace connection for a directory atomically.
107
+ */
108
+ export function writeWorkspaceConnection(directoryPath, connection) {
109
+ ensureModDir();
110
+ const connectionPath = getWorkspaceConnectionPath(directoryPath);
111
+ atomicWrite(connectionPath, JSON.stringify(connection, null, 2), 0o600);
112
+ }
113
+ /**
114
+ * Delete the workspace connection for a directory.
115
+ */
116
+ export function deleteWorkspaceConnection(directoryPath) {
117
+ const connectionPath = getWorkspaceConnectionPath(directoryPath);
118
+ if (!fs.existsSync(connectionPath)) {
119
+ return false;
120
+ }
121
+ try {
122
+ fs.unlinkSync(connectionPath);
123
+ return true;
124
+ }
125
+ catch (error) {
126
+ console.warn(`Warning: Could not delete workspace connection for ${directoryPath}`);
127
+ return false;
128
+ }
129
+ }
130
+ /**
131
+ * List all workspace connections.
132
+ * Returns array of connections, filtering out corrupted files.
133
+ */
134
+ export function listWorkspaceConnections() {
135
+ const workspacesDir = path.join(getModDir(), 'workspaces');
136
+ if (!fs.existsSync(workspacesDir)) {
137
+ return [];
138
+ }
139
+ const files = fs.readdirSync(workspacesDir);
140
+ const connections = [];
141
+ for (const file of files) {
142
+ const filePath = path.join(workspacesDir, file);
143
+ try {
144
+ const content = fs.readFileSync(filePath, 'utf-8');
145
+ const connection = JSON.parse(content);
146
+ connections.push(connection);
147
+ }
148
+ catch (error) {
149
+ // Skip corrupted files
150
+ continue;
151
+ }
152
+ }
153
+ return connections;
154
+ }
155
+ /**
156
+ * Find workspace connection for the current directory or any parent.
157
+ * Walks up the directory tree looking for a connection.
158
+ */
159
+ export function findWorkspaceConnection(startPath) {
160
+ let currentPath = path.resolve(startPath);
161
+ const root = path.parse(currentPath).root;
162
+ while (currentPath !== root) {
163
+ const connection = readWorkspaceConnection(currentPath);
164
+ if (connection) {
165
+ return connection;
166
+ }
167
+ currentPath = path.dirname(currentPath);
168
+ }
169
+ return null;
170
+ }
171
+ /**
172
+ * Atomic write: write to temp file then rename.
173
+ * Ensures file is never partially written.
174
+ */
175
+ function atomicWrite(filePath, content, mode) {
176
+ const tempPath = `${filePath}.tmp.${process.pid}`;
177
+ fs.writeFileSync(tempPath, content, { mode });
178
+ fs.renameSync(tempPath, filePath);
179
+ }
180
+ /**
181
+ * Get the path to the sync daemon PID file.
182
+ */
183
+ export function getPidFilePath() {
184
+ return path.join(getModDir(), 'sync.pid');
185
+ }
186
+ /**
187
+ * Get the path to the automerge storage directory.
188
+ */
189
+ export function getAutomergeStoragePath() {
190
+ return path.join(getModDir(), 'automerge');
191
+ }
192
+ /**
193
+ * Get the path to the logs directory.
194
+ */
195
+ export function getLogsDir() {
196
+ return path.join(getModDir(), 'logs');
197
+ }
198
+ /**
199
+ * Get the path to the sync daemon log file.
200
+ */
201
+ export function getSyncLogPath() {
202
+ return path.join(getLogsDir(), 'sync.log');
203
+ }